UNPKG

@itwin/measure-tools-react

Version:
467 lines 20.8 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { Geometry, IModelJson, Point3d, PointString3d, PolygonOps, } from "@itwin/core-geometry"; import { GraphicType, IModelApp } from "@itwin/core-frontend"; import { StyleSet, WellKnownGraphicStyleType } from "../api/GraphicStyle.js"; import { Measurement, MeasurementPickContext, MeasurementSerializer, } from "../api/Measurement.js"; import { MeasurementPropertyHelper } from "../api/MeasurementPropertyHelper.js"; import { MeasurementSelectionSet } from "../api/MeasurementSelectionSet.js"; import { Polygon } from "../api/Polygon.js"; import { DistanceMeasurement } from "./DistanceMeasurement.js"; import { MeasureTools } from "../MeasureTools.js"; /** Serializer for a [[AreaMeasurement]]. */ export class AreaMeasurementSerializer extends MeasurementSerializer { get measurementName() { return AreaMeasurementSerializer.areaMeasurementName; } isValidType(measurement) { return measurement instanceof AreaMeasurement; } isValidJSON(json) { if (!super.isValidJSON(json) || !json.hasOwnProperty("polygonPoints") || !Array.isArray(json.polygonPoints)) return false; return true; } parseSingle(data) { if (!this.isValidJSON(data)) return undefined; const props = data; return AreaMeasurement.fromJSON(props); } } AreaMeasurementSerializer.areaMeasurementName = "areaMeasurement"; /** * Area measurement. A polygon with formatted area in a "text pill" at the center. */ export class AreaMeasurement extends Measurement { get isDynamic() { return this._isDynamic; } set isDynamic(isDynamic) { this._isDynamic = isDynamic; this._polygon.makeSelectable(!isDynamic); this.clearCachedGraphics(); } get polygon() { return this._polygon; } get polygonPoints() { return this._polygon.points; } get isValidPolygon() { if (this.polygonPoints.length < 3) return false; const area = Math.abs(PolygonOps.area(this.polygonPoints)); if (0 >= area) return false; return true; } get lengthKoQ() { return this._lengthKoQ; } set lengthKoQ(value) { this._lengthKoQ = value; this._polygon.recomputeFromPoints(); } get lengthPersistenceUnitName() { return this._lengthPersistenceUnitName; } set lengthPersistenceUnitName(value) { this._lengthPersistenceUnitName = value; this._polygon.recomputeFromPoints(); } get areaKoQ() { return this._areaKoQ; } set areaKoQ(value) { this._areaKoQ = value; this._polygon.recomputeFromPoints(); } get areaPersistenceUnitName() { return this._areaPersistenceUnitName; } set areaPersistenceUnitName(value) { this._areaPersistenceUnitName = value; this._polygon.recomputeFromPoints(); } constructor(props) { super(props); this._polygon = new Polygon([], false, undefined, undefined, props?.formatting?.area); this._polygon.textMarker.setMouseButtonHandler(this.handleTextMarkerButtonEvent.bind(this)); this._lengthKoQ = "AecUnits.LENGTH"; this._lengthPersistenceUnitName = "Units.M"; this._areaKoQ = "AecUnits.AREA"; this._areaPersistenceUnitName = "Units.SQ_M"; this._polygon.textMarker.transientHiliteId = this.transientId; this._polygon.makeSelectable(true); this._isDynamic = false; if (props) this.readFromJSON(props); this.populateFormattingSpecsRegistry().catch(); } handleTextMarkerButtonEvent(ev) { if (this.isDynamic) return false; void this.onDecorationButtonEvent(MeasurementPickContext.createFromSourceId("Invalid", ev)); return true; } async populateFormattingSpecsRegistry(_force) { const lengthEntry = IModelApp.quantityFormatter.getSpecsByName(this._lengthKoQ); if (_force || !lengthEntry || lengthEntry.formatterSpec.persistenceUnit?.name !== this._lengthPersistenceUnitName) { const lengthFormatProps = await IModelApp.formatsProvider.getFormat(this._lengthKoQ); if (lengthFormatProps) { await IModelApp.quantityFormatter.addFormattingSpecsToRegistry(this._lengthKoQ, this._lengthPersistenceUnitName, lengthFormatProps); } } const areaEntry = IModelApp.quantityFormatter.getSpecsByName(this._areaKoQ); if (_force || !areaEntry || areaEntry.formatterSpec.persistenceUnit?.name !== this._areaPersistenceUnitName) { const areaFormatProps = await IModelApp.formatsProvider.getFormat(this._areaKoQ); if (areaFormatProps) { await IModelApp.quantityFormatter.addFormattingSpecsToRegistry(this._areaKoQ, this._areaPersistenceUnitName, areaFormatProps); } } } addPointToDynamicPolygon(point) { if (!this.isDynamic) return false; // Ignore the point if it's the same as the last one if (this.polygonPoints.length > 0 && this.polygonPoints[this.polygonPoints.length - 1].isAlmostEqual(point)) return false; this.polygonPoints.push(point.clone()); this.clearCachedGraphics(); // Check if we should close the polygon, if it's last and first points now equal if (this.polygonPoints.length > 2 && this.polygonPoints[0].isAlmostEqual(this.polygonPoints[this.polygonPoints.length - 1])) { this.closeDynamicPolygon(); return true; } return false; } updateDynamicPolygon(point) { if (!this.isDynamic) return; const length = this.polygonPoints.length; if (length === 0) return; const start = this.polygonPoints[length - 1]; this._dynamicEdge = DistanceMeasurement.create(start, point, undefined, { length: { koqName: this._lengthKoQ, persistenceUnitName: this._lengthPersistenceUnitName } }); if (this.drawingMetadata?.origin) this._dynamicEdge.drawingMetadata = { origin: this.drawingMetadata.origin, worldScale: this.worldScale }; this._dynamicEdge.sheetViewId = this.sheetViewId; this._dynamicEdge.viewTarget.copyFrom(this.viewTarget); this._dynamicEdge.style = this.style; this._dynamicEdge.lockStyle = this.lockStyle; this._dynamicEdge.isDynamic = true; this._dynamicEdge.showAxes = false; this._dynamicEdge.displayLabels = this.displayLabels; this._polygon.recomputeFromPoints(); } closeDynamicPolygon() { if (!this.isDynamic || !this.isValidPolygon) return false; const polyPoints = this.polygonPoints; const isClosedPath = polyPoints[0].isAlmostEqual(polyPoints[polyPoints.length - 1]); if (!isClosedPath) polyPoints.push(polyPoints[0].clone()); this._polygon.recomputeFromPoints(); this._dynamicEdge = undefined; this.isDynamic = false; return true; } testDecorationHit(pickContext) { if (this.transientId && this.transientId === pickContext.geomId) return true; if (pickContext.buttonEvent && this.displayLabels) return this._polygon.textMarker.pick(pickContext.buttonEvent.viewPoint); return false; } getDecorationGeometry(_pickContext) { if (this.polygonPoints.length === 0) return undefined; // If dynamic, only want to return the first snap point, because as we're laying out a dynamic polygon we want to be able to snap to itself at the first point if (this.isDynamic) { if (this.polygonPoints.length >= 3) return [ IModelJson.Writer.toIModelJson(PointString3d.create(this.polygonPoints[0])), ]; return undefined; } return [ IModelJson.Writer.toIModelJson(PointString3d.create(this.polygonPoints)), ]; } async getDecorationToolTip(_pickContext) { if (this.isDynamic) return MeasureTools.localization.getLocalizedString("MeasureTools:Measurements.closePolygon"); return MeasureTools.localization.getLocalizedString("MeasureTools:Measurements.areaMeasurement"); } getSnapId() { if (!this.transientId) this.transientId = MeasurementSelectionSet.nextTransientId; // We participate even during dynamics, so we can snap to the first point to close. But it only makes sense to do so if there are 3 or more points. if (this.isDynamic && this.polygonPoints.length < 3) return undefined; return this.transientId; } clearCachedGraphics() { if (this._cachedGraphic) { this._cachedGraphic.disposeGraphic(); this._cachedGraphic = undefined; } } onCleanup() { this.clearCachedGraphics(); } onTransientIdChanged(_prevId) { this._polygon.textMarker.transientHiliteId = this.transientId; this.clearCachedGraphics(); } addDynamicSnapGraphic(styleSet, context) { if (!this.isDynamic || this.polygonPoints.length === 0) return; // Add a graphic just for snapping to the first point if we can const firstPtSnapId = this.getSnapId(); if (firstPtSnapId === undefined) return; const builder = context.createGraphicBuilder(GraphicType.WorldOverlay, undefined, firstPtSnapId); // Make sure it's the same symbology as the polygon drawing code const style = styleSet .getGraphicStyle(WellKnownGraphicStyleType.AreaMeasurementDynamic) .clone(); style.lineWidth += 11; style.addStyledPointString(builder, [this.polygonPoints[0]], false); context.addDecorationFromBuilder(builder); } onDrawingMetadataChanged() { this.polygon.worldScale = this.worldScale; this._polygon.recomputeFromPoints(); } decorate(context) { super.decorate(context); if (this.polygonPoints.length === 0) return; const styleTheme = StyleSet.getOrDefault(this.activeStyle); const snapId = !this.isDynamic ? this.getSnapId() : undefined; if (this.isDynamic) { this.addDynamicSnapGraphic(styleTheme, context); if (this._dynamicEdge) this._dynamicEdge.decorate(context); const dynamicGBuilde = context.createGraphicBuilder(GraphicType.WorldOverlay); this.drawDynamicArea(styleTheme, dynamicGBuilde); context.addDecorationFromBuilder(dynamicGBuilde); if (!this._cachedGraphic) { const polygonGBuilder = context.createGraphicBuilder(GraphicType.WorldOverlay); this.drawDynamicPolygonShape(styleTheme, polygonGBuilder); this._cachedGraphic = IModelApp.renderSystem.createGraphicOwner(polygonGBuilder.finish()); } } else { if (!this._cachedGraphic) { const polygonGBuilder = context.createGraphicBuilder(GraphicType.WorldOverlay, undefined, snapId); this._polygon.addToGraphicBuilder(polygonGBuilder); this._cachedGraphic = IModelApp.renderSystem.createGraphicOwner(polygonGBuilder.finish()); } } if (this._cachedGraphic) context.addDecoration(GraphicType.WorldOverlay, this._cachedGraphic); if (0.0 < this._polygon.area && this.displayLabels) this._polygon.drawTextMarker(context); } drawDynamicArea(styleSet, graphicBuilder) { if (!this._dynamicEdge || this.polygonPoints.length === 0 || !this.polygon.drawFillArea) return; const first = this.polygonPoints[0]; const last = this._dynamicEdge.startPointRef; const dynamic = this._dynamicEdge.endPointRef; const pointsOnTemporaryShape = [first, last, dynamic]; const style = styleSet.getGraphicStyle(WellKnownGraphicStyleType.AreaMeasurementDynamic); style.addStyledShape(graphicBuilder, pointsOnTemporaryShape, false); } drawDynamicPolygonShape(styleSet, graphicBuilder) { if (this.polygonPoints.length === 0) return; // If drawing with fill, use the dynamic style. If not, use the regular style since it'll just be the outline (by default dynamic is lighter in color/transparency) const outlineStyleType = this.polygon.drawFillArea ? WellKnownGraphicStyleType.AreaMeasurementDynamic : WellKnownGraphicStyleType.AreaMeasurement; const style = styleSet.getGraphicStyle(outlineStyleType); style.addStyledPointString(graphicBuilder, this.polygonPoints, false); style.addStyledLineString(graphicBuilder, this.polygonPoints, false); if (this.polygon.drawFillArea) { const style2 = styleSet.getGraphicStyle(WellKnownGraphicStyleType.AreaMeasurement); style2.addStyledShape(graphicBuilder, this.polygonPoints, false); } } async getDataForMeasurementWidgetInternal() { const lengthSpec = IModelApp.quantityFormatter.getSpecsByName(this._lengthKoQ)?.formatterSpec; const areaSpec = IModelApp.quantityFormatter.getSpecsByName(this._areaKoQ)?.formatterSpec; const fPerimeter = IModelApp.quantityFormatter.formatQuantity(this.worldScale * this._polygon.perimeter, lengthSpec); const fArea = IModelApp.quantityFormatter.formatQuantity(this.worldScale * this.worldScale * this._polygon.area, areaSpec); const fAreaXY = IModelApp.quantityFormatter.formatQuantity(this._polygon.areaXY, areaSpec); const fEdgeCount = (this._polygon.points.length - 1).toFixed(); let title = MeasureTools.localization.getLocalizedString("MeasureTools:Measurements.areaMeasurement"); title += ` [${fArea}]`; const data = { title, properties: [] }; MeasurementPropertyHelper.tryAddNameProperty(this, data.properties); data.properties.push({ label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureArea.popupArea"), name: "AreaMeasurement_Area", value: fArea, aggregatableValue: areaSpec !== undefined ? { value: this._polygon.area, formatSpec: areaSpec } : undefined, }); if (this.drawingMetadata?.worldScale === undefined) { data.properties.push({ label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureArea.popupAreaXY"), name: "AreaMeasurement_AreaXY", value: fAreaXY, aggregatableValue: areaSpec !== undefined ? { value: this._polygon.areaXY, formatSpec: areaSpec } : undefined, }); } data.properties.push({ label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureArea.popupPerimeter"), name: "AreaMeasurement_Perimeter", value: fPerimeter, }, { label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureArea.popupEdgeCount"), name: "AreaMeasurement_EdgeCount", value: fEdgeCount, }); return data; } onStyleChanged(isLock, _prevStyle) { // Make sure polygon uses the active style this._polygon.styleSet = StyleSet.getOrDefault(this.activeStyle); this.clearCachedGraphics(); // Copy the style to the dynamic edge if (!this._dynamicEdge) return; if (isLock) this._dynamicEdge.lockStyle = this.lockStyle; else this._dynamicEdge.style = this.style; } onDisplayLabelsToggled() { if (this._dynamicEdge) this._dynamicEdge.displayLabels = this.displayLabels; } onLockToggled() { this._polygon.styleSet = StyleSet.getOrDefault(this.activeStyle); this.clearCachedGraphics(); } onDisplayUnitsChanged() { this._polygon.recomputeFromPoints(); } /** * Tests equality with another measurement. * @param other Measurement to test equality for. * @param opts Options for equality testing. * @returns true if the other measurement is equal, false if some property is not the same or if the measurement is not of the same type. */ equals(other, opts) { if (!super.equals(other, opts)) return false; // Compare data (ignore isDynamic) const tol = opts && opts.tolerance !== undefined ? opts.tolerance : Geometry.smallMetricDistance; const otherArea = other; if (otherArea === undefined || this.polygonPoints.length !== otherArea.polygonPoints.length) return false; const thisPts = this.polygonPoints; const otherPts = otherArea.polygonPoints; for (let i = 0; i < thisPts.length; i++) { const thisPt = thisPts[i]; const otherPt = otherPts[i]; if (!thisPt.isAlmostEqual(otherPt, tol)) return false; } return true; } /** * Copies data from the other measurement into this instance. * @param other Measurement to copy property values from. */ copyFrom(other) { super.copyFrom(other); if (other instanceof AreaMeasurement) { this.polygon.setPoints(other.polygonPoints, true, true); this.isDynamic = other.isDynamic; if (this.isDynamic && other._dynamicEdge) this.updateDynamicPolygon(other._dynamicEdge.endPointRef); } } /** * Deserializes properties (if they exist) from the JSON object. * @param json JSON object to read data from. */ readFromJSON(json) { super.readFromJSON(json); const jsonArea = json; if (jsonArea.formatting?.area?.koqName) { this._areaKoQ = jsonArea.formatting.area.koqName; this._polygon.areaKoQ = this._areaKoQ; } if (jsonArea.formatting?.area?.persistenceUnitName) { this._areaPersistenceUnitName = jsonArea.formatting.area.persistenceUnitName; this._polygon.areaPersistenceUnitName = this._areaPersistenceUnitName; } if (jsonArea.formatting?.length?.koqName) this._lengthKoQ = jsonArea.formatting.length.koqName; if (jsonArea.formatting?.length?.persistenceUnitName) this._lengthPersistenceUnitName = jsonArea.formatting.length.persistenceUnitName; if (jsonArea.polygonPoints !== undefined) { const pts = new Array(); for (const pt of jsonArea.polygonPoints) pts.push(Point3d.fromJSON(pt)); this._polygon.setPoints(pts, false, true); if (this.isDynamic && this._dynamicEdge) this.updateDynamicPolygon(this._dynamicEdge.endPointRef); } } /** * Serializes properties to a JSON object. * @param json JSON object to append data to. */ writeToJSON(json) { super.writeToJSON(json); const pts = new Array(); for (const pt of this.polygonPoints) pts.push(pt.toJSON()); const jsonArea = json; jsonArea.polygonPoints = pts; jsonArea.formatting = { length: { koqName: this._lengthKoQ, persistenceUnitName: this._lengthPersistenceUnitName, }, area: { koqName: this._areaKoQ, persistenceUnitName: this._areaPersistenceUnitName, }, }; } static create(pts, viewType, formatting) { // Don't ned to serialize the points, will just work as is const measurement = new AreaMeasurement({ polygonPoints: pts, formatting }); if (viewType) measurement.viewTarget.include(viewType); return measurement; } static fromJSON(data) { return new AreaMeasurement(data); } } AreaMeasurement.serializer = Measurement.registerSerializer(new AreaMeasurementSerializer()); //# sourceMappingURL=AreaMeasurement.js.map