import { Logger } from "@itwin/core-bentley" ; import { Cartographic } from "@itwin/core-common" ; import { BeButton , BeButtonEvent , Cluster , DecorateContext , imageElementFromUrl , IModelApp , InputSource , Marker , MarkerSet , NotifyMessageDetails , OutputMessagePriority , ScreenViewport , Tool , ViewState3d , } from "@itwin/core-frontend" ; import { Angle , Point2d , Point3d , Range2d , XYAndZ } from "@itwin/core-geometry" ; beta export interface GeoNameProps { name : string ; lat : number ; lng : number ; wikipedia ? : string ; population ? : number ; } /** Marker positioned where there is a geographic entity. */ class GeoNameMarker extends Marker { private static _size = Point2d . create ( 20 , 20 ) ; private static _imageSize = Point2d . create ( 30 , 30 ) ; constructor ( location , public props , icon ) { super ( location , GeoNameMarker . _size ) ; this . setImage ( icon ) ; this . imageSize = GeoNameMarker . _imageSize ; this . setScaleFactor ( { low : 1 , high : 1 } ) ; this . labelOffset = { x : 0 , y : - 24 } ; this . title = props . name ; if ( props . population ) this . title = `${ this . title } (${ IModelApp . localization . getLocalizedString ( "geoNames:misc.Population" ) }: ${ props . population })` ; this . label = props . name ; } public override onMouseButton ( ev ) { if ( InputSource . Mouse === ev . inputSource && ev . isDown && ev . viewport !== undefined && ev . viewport . view instanceof ViewState3d ) { if ( BeButton . Data === ev . button ) { const evViewport = ev . viewport ; ( async ( ) => { await evViewport . animateFlyoverToGlobalLocation ( { center : Cartographic . fromRadians ( { longitude : this . props . lng * Angle . radiansPerDegree , latitude : this . props . lat * Angle . radiansPerDegree } ) } ) ; } ) ( ) . catch ( ( ) => { } ) ; } else if ( BeButton . Reset === ev . button && undefined !== this . props . wikipedia && 0 !== this . props . wikipedia . length ) window . open ( `https://${ this . props . wikipedia }` ) ; } return true ; } } class GeoNameMarkerSet extends MarkerSet < GeoNameMarker > { public override minimumClusterSize = 5 ; protected getClusterMarker ( cluster ) { return new Marker ( cluster . getClusterLocation ( ) , cluster . markers [ 0 ] . size ) ; } } export class GeoNameMarkerManager { private _markerSet : GeoNameMarkerSet ; public static decorator ? : GeoNameMarkerManager ; private static _scratchCarto = Cartographic . createZero ( ) ; private static _scratchPoint = Point3d . createZero ( ) ; public constructor ( vp , private _cityMarkerImage , private _cityCount = 50 ) { this . _markerSet = new GeoNameMarkerSet ( vp ) ; } public decorate ( context ) { if ( this . _markerSet !== undefined ) this . _markerSet . addDecoration ( context ) ; } private synch ( viewport ) { const currentViewport = this . _markerSet . viewport ; if ( currentViewport !== viewport ) this . _markerSet . changeViewport ( viewport ) ; const view = viewport . view as ViewState3d ; const worldFrust = viewport . getFrustum ( ) ; const longLatRange = Range2d . createNull ( ) ; for ( const corner of worldFrust . points ) { const carto = view . rootToCartographic ( corner ) ; if ( undefined !== carto ) longLatRange . extendXY ( carto . longitude , carto . latitude ) ; } this . doCitySearch ( longLatRange , this . _cityCount ) . then ( ( cities ) => { if ( cities !== undefined ) { for ( const city of cities ) { GeoNameMarkerManager . _scratchCarto . longitude = city . lng * Angle . radiansPerDegree ; GeoNameMarkerManager . _scratchCarto . latitude = city . lat * Angle . radiansPerDegree ; this . _markerSet . markers . add ( new GeoNameMarker ( view . cartographicToRoot ( GeoNameMarkerManager . _scratchCarto , GeoNameMarkerManager . _scratchPoint ) ! , city , this . _cityMarkerImage ) ) ; } this . _markerSet . markDirty ( ) ; IModelApp . viewManager . invalidateDecorationsAllViews ( ) ; IModelApp . requestNextAnimation ( ) ; } } ) . catch ( ( ) => { } ) ; } private static async loadImage ( src ) { try { return await imageElementFromUrl ( src ) ; } catch ( err ) { Logger . logError ( "SectionLocationSetDecoration" , `Could not load image ${ src }` ) ; } return undefined ; } private outputInfoMessage ( messageKey ) { const message = IModelApp . localization . getLocalizedString ( `geoNames:messages.${ messageKey }` ) ; const msgDetails = new NotifyMessageDetails ( OutputMessagePriority . Info , message ) ; IModelApp . notifications . outputMessage ( msgDetails ) ; } private radiansToString ( radians ) { return ( radians * Angle . degreesPerRadian ) . toFixed ( 5 ) ; } private async doCitySearch ( longLatRange , cityCount ) { const urlTemplate = "http://api.geonames.org/citiesJSON?&north={north}&south={south}&east={east}&west={west}&lang=en&username=BentleySystems&maxRows={count}" ; const url = urlTemplate . replace ( "{west}" , this . radiansToString ( longLatRange . low . x ) ) . replace ( "{south}" , this . radiansToString ( longLatRange . low . y ) ) . replace ( "{east}" , this . radiansToString ( longLatRange . high . x ) ) . replace ( "{north}" , this . radiansToString ( longLatRange . high . y ) ) . replace ( "{count}" , cityCount . toString ( ) ) ; try { this . outputInfoMessage ( "LoadingLocations" ) ; let json = await fetch ( url , { method : "GET" , headers : { "Content-Type" : "application/json" } } ) ; json = json . json ( ) ; const cities = new Array < GeoNameProps > ( ) ; for ( const geoName of json . geonames ) { cities . push ( geoName ) ; } this . outputInfoMessage ( "LoadingComplete" ) ; return cities ; } catch ( error ) { return undefined ; } } /** Start showing markers if not currently active (or optionally refresh when currently displayed). */ public static async show ( vp ) { if ( undefined === GeoNameMarkerManager . decorator ) { const cityMarkerImage = await this . loadImage ( "./city.ico" ) ; if ( undefined === cityMarkerImage ) return ; GeoNameMarkerManager . decorator = new GeoNameMarkerManager ( vp , cityMarkerImage ) ; IModelApp . viewManager . addDecorator ( GeoNameMarkerManager . decorator ) ; } GeoNameMarkerManager . decorator . synch ( vp ) ; } public static clear ( _vp ) { if ( undefined !== GeoNameMarkerManager . decorator ) { IModelApp . viewManager . dropDecorator ( GeoNameMarkerManager . decorator ) ; GeoNameMarkerManager . decorator = undefined ; } } public static update ( vp ) { if ( undefined !== GeoNameMarkerManager . decorator ) { GeoNameMarkerManager . decorator . synch ( vp ) ; } } } /** An Immediate Tool that attempts to use the geoLocation API to find the given feature */ abstract class GeoNameTool extends Tool { public static override get maxArgs ( ) { return 1 ; } public static override get minArgs ( ) { return 0 ; } public abstract doRunWithViewport ( vp ) ; public override async run ( viewport ? ) { if ( undefined === viewport ) viewport = IModelApp . viewManager . selectedView ; if ( undefined !== viewport ) this . doRunWithViewport ( viewport ) ; return true ; } } class GeoNameOnTool extends GeoNameTool { public static override toolId = "GeoNamesOnTool" ; public doRunWithViewport ( vp ) { GeoNameMarkerManager . show ( vp ) . then ( ( ) => { } ) . catch ( ( ) => { } ) ; } } class GeoNameOffTool extends GeoNameTool { public static override toolId = "GeoNamesOffTool" ; public doRunWithViewport ( vp ) { GeoNameMarkerManager . clear ( vp ) ; } } class GeoNameUpdateTool extends GeoNameTool { public static override toolId = "GeoNamesUpdateTool" ; public doRunWithViewport ( vp ) { GeoNameMarkerManager . update ( vp ) ; } } export class GeoNameExtension { private static _defaultNs = "mapLayers" ; public static async initialize ( ) { await IModelApp . localization . registerNamespace ( this . _defaultNs ) ; IModelApp . tools . register ( GeoNameOnTool , this . _defaultNs ) ; IModelApp . tools . register ( GeoNameOffTool , this . _defaultNs ) ; IModelApp . tools . register ( GeoNameUpdateTool , this . _defaultNs ) ; if ( undefined !== IModelApp . viewManager . selectedView ) await GeoNameMarkerManager . show ( IModelApp . viewManager . selectedView ) ; } }