import { Logger } from "@itwin/core-bentley" ; import { AngleSweep , Arc3d , Point2d , Point3d , XAndY , XYAndZ } from "@itwin/core-geometry" ; import { AxisAlignedBox3d , ColorByName , ColorDef } from "@itwin/core-common" ; import { BeButton , BeButtonEvent , Cluster , DecorateContext , GraphicType , imageElementFromUrl , IModelApp , Marker , MarkerImage , MarkerSet , MessageBoxIconType , MessageBoxType , Tool , } from "@itwin/core-frontend" ; /** Example Marker to show an *incident*. Each incident has an *id*, a *severity*, and an *icon*. */ class IncidentMarker extends Marker { private static _size = Point2d . create ( 30 , 30 ) ; private static _imageSize = Point2d . create ( 40 , 40 ) ; private static _imageOffset = Point2d . create ( 0 , 30 ) ; private static _amber = ColorDef . create ( ColorByName . amber ) ; private static _sweep360 = AngleSweep . create360 ( ) ; private _color : ColorDef ; /** uncomment the next line to make the icon only show when the cursor is over an incident marker. */ /** Get a color based on severity by interpolating Green(0) -> Amber(15) -> Red(30)  */ public static makeColor ( severity ) { return ( severity <= 16 ? ColorDef . green . lerp ( this . _amber , ( severity - 1 ) / 15. ) : this . _amber . lerp ( ColorDef . red , ( severity - 16 ) / 14. ) ) ; } public override onMouseButton ( ev ) { if ( ev . button === BeButton . Data && ev . isDown ) { IModelApp . notifications . openMessageBox ( MessageBoxType . LargeOk , `severity = ${ this . severity }` , MessageBoxIconType . Information ) ; } return true ; } /** Create a new IncidentMarker */ constructor ( location , public severity , public id , icon ) { super ( location , IncidentMarker . _size ) ; this . _color = IncidentMarker . makeColor ( severity ) ; this . setImage ( icon ) ; this . imageOffset = IncidentMarker . _imageOffset ; this . imageSize = IncidentMarker . _imageSize ; this . title = `Severity: ${ severity }<br>Id: ${ id }` ; this . setScaleFactor ( { low : .2 , high : 1.4 } ) ; this . label = id . toString ( ) ; } /**
     * For this demo, add a WorldDecoration that draws a circle with a radius of 200cm centered at the incident location.
     * WorldDecorations are in world coordinates, so the circle will change size as you zoom in/out. Also, they are drawn with the z-buffer enabled, so
     * the circle may be obscured by other geometry in front of in the view. This can help the user understand the point that the marker relates to,
     * but that effect isn't always desireable.
     *
     * World decorations for markers are completely optional. If you don't want anything drawn with WorldDecorations, don't follow this example.
     *
     */ public override addMarker ( context ) { super . addMarker ( context ) ; const builder = context . createGraphicBuilder ( GraphicType . WorldDecoration ) ; const ellipse = Arc3d . createScaledXYColumns ( this . worldLocation , context . viewport . rotation . transpose ( ) , .2 , .2 , IncidentMarker . _sweep360 ) ; let color = this . _color ; builder . setSymbology ( ColorDef . white , color , 1 ) ; color = color . withTransparency ( 200 ) ; builder . addArc ( ellipse , false , false ) ; builder . setBlankingFill ( color ) ; builder . addArc ( ellipse , true , true ) ; context . addDecorationFromBuilder ( builder ) ; } } /** A Marker used to show a cluster of incidents */ class IncidentClusterMarker extends Marker { private _clusterColor : string ; public override drawFunc ( ctx ) { ctx . beginPath ( ) ; ctx . strokeStyle = this . _clusterColor ; ctx . fillStyle = "white" ; ctx . lineWidth = 5 ; ctx . arc ( 0 , 0 , 13 , 0 , Math . PI * 2 ) ; ctx . fill ( ) ; ctx . stroke ( ) ; } /** Create a new cluster marker with label and color based on the content of the cluster */ constructor ( location , size , cluster , image ) { super ( location , size ) ; const sorted = [ ] ; const maxLen = 10 ; cluster . markers . forEach ( ( marker ) => { if ( maxLen > sorted . length || marker . severity > sorted [ sorted . length - 1 ] . severity ) { const index = sorted . findIndex ( ( val ) => val . severity < marker . severity ) ; if ( index === - 1 ) sorted . push ( marker ) ; else sorted . splice ( index , 0 , marker ) ; if ( sorted . length > maxLen ) sorted . length = maxLen ; } } ) ; this . imageOffset = new Point3d ( 0 , 28 ) ; this . imageSize = new Point2d ( 30 , 30 ) ; this . label = cluster . markers . length . toLocaleString ( ) ; this . labelColor = "black" ; this . labelFont = "bold 14px sans-serif" ; this . setScaleFactor ( { low : .7 , high : 1.2 } ) ; let title = "" ; sorted . forEach ( ( marker ) => { if ( title !== "" ) title += "<br>" ; title += `Severity: ${ marker . severity } Id: ${ marker . id }` ; } ) ; if ( cluster . markers . length > maxLen ) title += "<br>..." ; const div = document . createElement ( "div" ) ; div . innerHTML = title ; this . title = div ; this . _clusterColor = IncidentMarker . makeColor ( sorted [ 0 ] . severity ) . toHexString ( ) ; if ( image ) this . setImage ( image ) ; } } /** A MarkerSet to hold incidents. This class supplies to `getClusterMarker` method to create IncidentClusterMarkers. */ class IncidentMarkerSet extends MarkerSet < IncidentMarker > { protected getClusterMarker ( cluster ) { return new IncidentClusterMarker ( cluster . getClusterLocation ( ) , cluster . markers [ 0 ] . size , cluster , IncidentMarkerDemo . decorator ! . warningSign ) ; } } /** This demo shows how to use MarkerSets to cluster markers that overlap on the screen. It creates a set of 500
 * "incidents" at random locations within the ProjectExtents. For each incident, it creates an IncidentMarker with an Id and
 * with a random value between 1-30 for "severity", and one of 5 possible icons.
 */ export class IncidentMarkerDemo { private _awaiting = false ; private _loading ? : Promise < any > ; private _images : Array < HTMLImageElement | undefined > = [ ] ; public readonly incidents = new IncidentMarkerSet ( ) ; private static _numMarkers = 500 ; public static decorator ? : IncidentMarkerDemo ; public get warningSign ( ) { return this . _images [ 0 ] ; } private async loadOne ( src ) { try { return await imageElementFromUrl ( src ) ; } catch ( err ) { const msg = `Could not load image ${ src }` ; Logger . logError ( "IncidentDemo" , msg ) ; console . log ( msg ) ; } return undefined ; } private async loadAll ( extents ) { const loads = [ this . loadOne ( "Warning_sign.svg" ) , this . loadOne ( "Hazard_biological.svg" ) , this . loadOne ( "Hazard_electric.svg" ) , this . loadOne ( "Hazard_flammable.svg" ) , this . loadOne ( "Hazard_toxic.svg" ) , this . loadOne ( "Hazard_tripping.svg" ) , ] ; await ( this . _loading = Promise . all ( loads ) ) ; for ( const img of loads ) this . _images . push ( await img ) ; const len = this . _images . length ; const pos = new Point3d ( ) ; for ( let i = 0 ; i < IncidentMarkerDemo . _numMarkers ; ++ i ) { pos . x = extents . low . x + ( Math . random ( ) * extents . xLength ( ) ) ; pos . y = extents . low . y + ( Math . random ( ) * extents . yLength ( ) ) ; pos . z = extents . low . z + ( Math . random ( ) * extents . zLength ( ) ) ; const img = this . _images [ ( i % len ) + 1 ] ; if ( undefined !== img ) this . incidents . markers . add ( new IncidentMarker ( pos , 1 + Math . round ( Math . random ( ) * 29 ) , i , img ) ) ; } this . _loading = undefined ; } public constructor ( extents ) { this . loadAll ( extents ) ; } /** This will allow the render system to cache and reuse the decorations created by this decorator's decorate() method. */ public readonly useCachedDecorations = true ; /** We added this class as a ViewManager.decorator below. This method is called to ask for our decorations. We add the MarkerSet. */ public decorate ( context ) { if ( ! context . viewport . view . isSpatialView ( ) ) return ; if ( undefined === this . _loading ) { this . incidents . addDecoration ( context ) ; return ; } if ( ! this . _awaiting ) { this . _awaiting = true ; this . _loading . then ( ( ) => { context . viewport . invalidateCachedDecorations ( this ) ; this . _awaiting = false ; } ) . catch ( ( ) => undefined ) ; } } /** start the demo by creating the IncidentMarkerDemo object and adding it as a ViewManager decorator. */ private static start ( extents ) { IncidentMarkerDemo . decorator = new IncidentMarkerDemo ( extents ) ; IModelApp . viewManager . addDecorator ( IncidentMarkerDemo . decorator ) ; IncidentMarkerDemo . decorator . incidents . viewport ! . onChangeView . addOnce ( ( ) => this . stop ( ) ) ; } /** stop the demo */ private static stop ( ) { if ( IncidentMarkerDemo . decorator ) IModelApp . viewManager . dropDecorator ( IncidentMarkerDemo . decorator ) ; IncidentMarkerDemo . decorator = undefined ; } /** Turn the markers on and off. Each time it runs it creates a new random set of incidents. */ public static toggle ( extents ) { if ( undefined === IncidentMarkerDemo . decorator ) this . start ( extents ) ; else this . stop ( ) ; } } export class IncidentMarkerDemoTool extends Tool { public static override toolId = "ToggleIncidentMarkers" ; public override async run ( _args ) { const vp = IModelApp . viewManager . selectedView ; if ( undefined !== vp && vp . view . isSpatialView ( ) ) IncidentMarkerDemo . toggle ( vp . view . iModel . projectExtents ) ; return true ; } }