@itwin/measure-tools-react
Version:
Frontend framework and tools for measurements
462 lines • 20.5 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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, } from "@itwin/core-geometry";
import { Cartographic } from "@itwin/core-common";
import { GraphicType, IModelApp } from "@itwin/core-frontend";
import { FormatterUtils } from "../api/FormatterUtils.js";
import { StyleSet, WellKnownGraphicStyleType, WellKnownTextStyleType, } from "../api/GraphicStyle.js";
import { Measurement, MeasurementPickContext, MeasurementSerializer, } from "../api/Measurement.js";
import { WellKnownViewType } from "../api/MeasurementEnums.js";
import { MeasurementPreferences } from "../api/MeasurementPreferences.js";
import { MeasurementPropertyHelper } from "../api/MeasurementPropertyHelper.js";
import { MeasurementSelectionSet } from "../api/MeasurementSelectionSet.js";
import { TextMarker } from "../api/TextMarker.js";
import { MeasureTools } from "../MeasureTools.js";
/** Serializer for a [[LocationMeasurement]]. */
export class LocationMeasurementSerializer extends MeasurementSerializer {
get measurementName() {
return LocationMeasurementSerializer.locationMeasurementName;
}
isValidType(measurement) {
return measurement instanceof LocationMeasurement;
}
isValidJSON(json) {
if (!super.isValidJSON(json) || !json.hasOwnProperty("location"))
return false;
return true;
}
parseSingle(data) {
if (!this.isValidJSON(data))
return undefined;
const props = data;
return LocationMeasurement.fromJSON(props);
}
}
LocationMeasurementSerializer.locationMeasurementName = "locationMeasurement";
/**
* Location measurement. A point somewhere in the world, optionally with other values (such as slope, station, offset, etc).
*/
export class LocationMeasurement extends Measurement {
get location() {
return this._location;
}
set location(pt) {
this._location.setFrom(pt);
this.createTextMarker().catch(); // eslint-disable-line @typescript-eslint/no-floating-promises
}
get geoLocation() {
return this._geoLocation;
}
set geoLocation(geoLoc) {
this._geoLocation = geoLoc;
}
get slope() {
return this._slope;
}
set slope(slope) {
this._slope = slope;
}
get station() {
return this._station;
}
set station(station) {
this._station = station;
}
get offset() {
return this._offset;
}
set offset(offset) {
this._offset = offset;
}
get isDynamic() {
return this._isDynamic;
}
set isDynamic(v) {
this._isDynamic = v;
if (this._textMarker)
this._textMarker.pickable = !v;
}
get lengthKoQ() {
return this._lengthKoQ;
}
set lengthKoQ(value) {
this._lengthKoQ = value;
this.createTextMarker().catch(); // eslint-disable-line @typescript-eslint/no-floating-promises
}
get lengthPersistenceUnitName() {
return this._lengthPersistenceUnitName;
}
set lengthPersistenceUnitName(value) {
this._lengthPersistenceUnitName = value;
this.createTextMarker().catch(); // eslint-disable-line @typescript-eslint/no-floating-promises
}
get stationKoQ() {
return this._stationKoQ;
}
set stationKoQ(value) {
this._stationKoQ = value;
}
get stationPersistenceUnitName() {
return this._stationPersistenceUnitName;
}
set stationPersistenceUnitName(value) {
this._stationPersistenceUnitName = value;
}
get angleKoQ() {
return this._angleKoQ;
}
set angleKoQ(value) {
this._angleKoQ = value;
}
get anglePersistenceUnitName() {
return this._anglePersistenceUnitName;
}
set anglePersistenceUnitName(value) {
this._anglePersistenceUnitName = value;
}
constructor(props) {
super(props);
this._location = Point3d.createZero();
this._isDynamic = false;
this._lengthKoQ = "AecUnits.LENGTH";
this._lengthPersistenceUnitName = "Units.M";
this._stationKoQ = "RoadRailUnits.STATION";
this._stationPersistenceUnitName = "Units.M";
this._angleKoQ = "AecUnits.ANGLE";
this._anglePersistenceUnitName = "Units.RAD";
this.getSnapId(); // Preload transient ID"s since we normally don not have these as dynamic
if (props) {
this.readFromJSON(props);
}
this.populateFormattingSpecsRegistry().then(() => this.createTextMarker().catch())
.catch();
}
/** Changes the location. Only possible if the measurement is dynamic. */
changeLocation(props) {
if (!this.isDynamic)
return false;
this.readFromJSON(props);
return true;
}
testDecorationHit(pickContext) {
if (this.transientId && this.transientId === pickContext.geomId)
return true;
if (pickContext.buttonEvent && this._textMarker && this.displayLabels)
return this._textMarker.pick(pickContext.buttonEvent.viewPoint);
return false;
}
getDecorationGeometry(_pickContext) {
return [
IModelJson.Writer.toIModelJson(PointString3d.create(this.location)),
];
}
async getDecorationToolTip(_pickContext) {
return MeasureTools.localization.getLocalizedString("MeasureTools:Measurements.locationMeasurement");
}
getSnapId() {
if (!this.transientId)
this.transientId = MeasurementSelectionSet.nextTransientId;
if (this.isDynamic)
return undefined;
return this.transientId;
}
onTransientIdChanged(_prevId) {
if (this._textMarker)
this._textMarker.transientHiliteId = this.transientId;
}
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 stationEntry = IModelApp.quantityFormatter.getSpecsByName(this._stationKoQ);
if (_force || !stationEntry || stationEntry.formatterSpec.persistenceUnit?.name !== this._stationPersistenceUnitName) {
const stationFormatProps = await IModelApp.formatsProvider.getFormat(this._stationKoQ);
if (stationFormatProps) {
await IModelApp.quantityFormatter.addFormattingSpecsToRegistry(this._stationKoQ, this._stationPersistenceUnitName, stationFormatProps);
}
}
const angleEntry = IModelApp.quantityFormatter.getSpecsByName(this._angleKoQ);
if (_force || !angleEntry || angleEntry.formatterSpec.persistenceUnit?.name !== this._anglePersistenceUnitName) {
const angleFormatProps = await IModelApp.formatsProvider.getFormat(this._angleKoQ);
if (angleFormatProps) {
await IModelApp.quantityFormatter.addFormattingSpecsToRegistry(this._angleKoQ, this._anglePersistenceUnitName, angleFormatProps);
}
}
}
decorate(context) {
super.decorate(context);
const styleTheme = StyleSet.getOrDefault(this.activeStyle);
const style = styleTheme.getGraphicStyle(WellKnownGraphicStyleType.LocationMeasurement);
if (this._textMarker && this.displayLabels)
this._textMarker.addDecoration(context);
if (this.isDynamic)
return;
const xBuilder = context.createGraphicBuilder(GraphicType.WorldDecoration, undefined, this.getSnapId());
style.addStyledPointString(xBuilder, [this._location], false);
context.addDecorationFromBuilder(xBuilder);
}
async createTextMarker() {
const adjustedLocation = this.adjustPointWithSheetToWorldTransform(this.adjustPointForGlobalOrigin(this._location));
const lengthSpec = IModelApp.quantityFormatter.getSpecsByName(this._lengthKoQ)?.formatterSpec;
const entries = [
{
label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureLocation.coordinate_x"),
value: await FormatterUtils.formatLength(adjustedLocation.x, lengthSpec),
},
{
label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureLocation.coordinate_y"),
value: await FormatterUtils.formatLength(adjustedLocation.y, lengthSpec),
},
{
label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureLocation.coordinate_z"),
value: await FormatterUtils.formatLength(adjustedLocation.z, lengthSpec),
},
];
if (this._isDynamic) {
entries.push({
label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureLocation.slope"),
value: this._slope === undefined ? "" : FormatterUtils.formatSlope(100 * this._slope, false),
});
}
if (!this._textMarker) {
const styleTheme = StyleSet.getOrDefault(this.activeStyle);
this._textMarker = TextMarker.createHoverBox(entries, this._location, styleTheme);
this._textMarker.pickable = !this.isDynamic;
this._textMarker.transientHiliteId = this.transientId;
this._textMarker.setMouseButtonHandler(this.handleTextMarkerButtonEvent.bind(this));
}
else {
this._textMarker.pickable = !this.isDynamic;
this._textMarker.worldLocation = this._location;
this._textMarker.textLines = entries;
}
}
async getDataForMeasurementWidgetInternal() {
const lengthSpec = IModelApp.quantityFormatter.getSpecsByName(this._lengthKoQ)?.formatterSpec;
const angleSpec = IModelApp.quantityFormatter.getSpecsByName(this._angleKoQ)?.formatterSpec;
const stationSpec = IModelApp.quantityFormatter.getSpecsByName(this._stationKoQ)?.formatterSpec;
const adjustedLocation = this.adjustPointWithSheetToWorldTransform(this.adjustPointForGlobalOrigin(this._location));
const fCoordinates = FormatterUtils.formatCoordinatesImmediate(adjustedLocation, lengthSpec);
let title = MeasureTools.localization.getLocalizedString("MeasureTools:Measurements.locationMeasurement");
title += ` [${fCoordinates}]`;
const data = { title, properties: [] };
MeasurementPropertyHelper.tryAddNameProperty(this, data.properties);
data.properties.push({
label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureLocation.coordinates"),
name: "LocationMeasurement_Location",
value: fCoordinates,
});
if (this._geoLocation && this.drawingMetadata?.sheetToWorldTransform === undefined)
data.properties.push({
label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureLocation.latLong"),
name: "LocationMeasurement_LatLong",
value: await FormatterUtils.formatCartographicToLatLong(this._geoLocation, angleSpec),
});
if (MeasurementPreferences.current.displayLocationAltitude) {
data.properties.push({
label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureLocation.altitude"),
name: "LocationMeasurement_Altitude",
value: await FormatterUtils.formatLength(adjustedLocation.z, lengthSpec),
});
}
if (this.drawingMetadata?.sheetToWorldTransform === undefined) {
let slopeValue;
if (undefined !== this._slope)
slopeValue = FormatterUtils.formatSlope(100.0 * this._slope, true);
else
slopeValue = MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureLocation.slopeUnavailable");
data.properties.push({
label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureLocation.slope"),
name: "LocationMeasurement_Slope",
value: slopeValue,
});
}
if (undefined !== this._station) {
data.properties.push({
label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureLocation.station"),
name: "LocationMeasurement_Station",
value: await FormatterUtils.formatStation(this._station, stationSpec),
});
}
if (undefined !== this._offset) {
data.properties.push({
label: MeasureTools.localization.getLocalizedString("MeasureTools:tools.MeasureLocation.offset"),
name: "LocationMeasurement_Offset",
value: await FormatterUtils.formatLength(this._offset, lengthSpec),
});
}
return data;
}
handleTextMarkerButtonEvent(ev) {
if (this._isDynamic)
return false;
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.onDecorationButtonEvent(MeasurementPickContext.createFromSourceId("Invalid", ev)).catch();
return true;
}
onStyleChanged(_isLock, _prevStyle) {
this.updateMarkerStyle();
}
onLockToggled() {
this.updateMarkerStyle();
}
onDisplayUnitsChanged() {
this.createTextMarker().catch(); // eslint-disable-line @typescript-eslint/no-floating-promises
}
updateMarkerStyle() {
if (!this._textMarker)
return;
const styleTheme = StyleSet.getOrDefault(this.activeStyle);
const tStyle = styleTheme.getTextStyle(WellKnownTextStyleType.HoverBox);
this._textMarker.applyStyle(tStyle);
}
/**
* 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 otherLoc = other;
if (otherLoc === undefined ||
!this._location.isAlmostEqual(otherLoc._location, tol) ||
!isNearlyEqual(this._offset, otherLoc._offset, tol) ||
!isNearlyEqual(this._slope, otherLoc._slope, tol) ||
!isNearlyEqual(this._station, otherLoc._station, tol))
return false;
if (this._geoLocation !== undefined &&
otherLoc._geoLocation !== undefined) {
if (!this._geoLocation.equalsEpsilon(otherLoc._geoLocation, tol))
return false;
}
else if (this._geoLocation !== otherLoc._geoLocation) {
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 LocationMeasurement) {
this._isDynamic = other._isDynamic;
this._location.setFrom(other._location);
this._geoLocation = other._geoLocation
? other._geoLocation.clone()
: undefined;
this._slope = other._slope;
this._offset = other._offset;
this._station = other._station;
this.createTextMarker().catch(); // eslint-disable-line @typescript-eslint/no-floating-promises
}
}
/**
* Deserializes properties (if they exist) from the JSON object.
* @param json JSON object to read data from.
*/
readFromJSON(json) {
super.readFromJSON(json);
const jsonLoc = json;
if (jsonLoc.location !== undefined)
this._location.setFromJSON(jsonLoc.location);
if (jsonLoc.geoLocation)
this._geoLocation = Cartographic.fromRadians(jsonLoc.geoLocation);
else
this._geoLocation = undefined;
if (jsonLoc.formatting?.length?.koqName)
this._lengthKoQ = jsonLoc.formatting.length.koqName;
if (jsonLoc.formatting?.length?.persistenceUnitName)
this._lengthPersistenceUnitName = jsonLoc.formatting.length.persistenceUnitName;
if (jsonLoc.formatting?.station?.koqName)
this._stationKoQ = jsonLoc.formatting.station.koqName;
if (jsonLoc.formatting?.station?.persistenceUnitName)
this._stationPersistenceUnitName = jsonLoc.formatting.station.persistenceUnitName;
if (jsonLoc.formatting?.angle?.koqName)
this._angleKoQ = jsonLoc.formatting.angle.koqName;
if (jsonLoc.formatting?.angle?.persistenceUnitName)
this._anglePersistenceUnitName = jsonLoc.formatting.angle.persistenceUnitName;
this._slope = jsonLoc.slope;
this._station = jsonLoc.station;
this._offset = jsonLoc.offset;
this.createTextMarker().catch(); // eslint-disable-line @typescript-eslint/no-floating-promises
}
/**
* Serializes properties to a JSON object.
* @param json JSON object to append data to.
*/
writeToJSON(json) {
super.writeToJSON(json);
const jsonLoc = json;
jsonLoc.location = this._location.toJSON();
if (this._geoLocation) {
const geoLoc = this._geoLocation;
jsonLoc.geoLocation = {
latitude: geoLoc.latitude,
longitude: geoLoc.longitude,
height: geoLoc.height,
};
}
else {
jsonLoc.geoLocation = undefined;
}
jsonLoc.slope = this._slope;
jsonLoc.station = this._station;
jsonLoc.offset = this._offset;
jsonLoc.formatting = {
length: {
koqName: this._lengthKoQ,
persistenceUnitName: this._lengthPersistenceUnitName,
},
station: {
koqName: this._stationKoQ,
persistenceUnitName: this._stationPersistenceUnitName,
},
angle: {
koqName: this._angleKoQ,
persistenceUnitName: this._anglePersistenceUnitName,
},
};
}
static create(location, viewType, formatting) {
// Don't need to serialize the points, will just work as is
const measurement = new LocationMeasurement({ location, formatting });
if (viewType)
measurement.viewTarget.include(viewType);
return measurement;
}
static fromJSON(props) {
const locMeasurement = new LocationMeasurement(props);
// LEGACY - Originally location measurements were hardcoded for "MainOnly", so if no viewTarget/viewportType default to "Spatial". So reading this from old JSON
// we don"t want location measurement"s set to Any...but we want new measurements created to be set to Any if no view types are given!
if (props.viewTarget === undefined &&
props.viewportType === undefined)
locMeasurement.viewTarget.include(WellKnownViewType.Spatial);
return locMeasurement;
}
}
LocationMeasurement.serializer = Measurement.registerSerializer(new LocationMeasurementSerializer());
function isNearlyEqual(a, b, tol) {
if (a !== undefined && b !== undefined) {
if (!Geometry.isSameCoordinate(a, b, tol))
return false;
}
else if (a !== b) {
return false;
}
return true;
}
//# sourceMappingURL=LocationMeasurement.js.map