import { CanvasWidget } from "@hpcc-js/common" ; import { quadtree as d3quadtree } from "d3-quadtree" ; export interface CanvasPinRow { 0 : number ; 1 : number ; is_cluster : boolean ; already_flagged : boolean ; weight : number ; fillStyle : string ; strokeStyle : string ; overlap_arr : CanvasPinRow [ ] ; } interface Rect { left : number ; right : number ; top : number ; bottom : number ; } class Quadtree { protected _pin_h = 0 ; protected _pin_w = 0 ; protected _tree : any ; constructor ( extent , raw_data , pin_h , pin_w ) { this . _pin_h = pin_h ; this . _pin_w = pin_w ; this . _tree = d3quadtree ( ) . extent ( extent ) . addAll ( raw_data ) ; } searchRect ( left , top , right , bottom ) { const ret = [ ] ; this . _tree . visit ( ( node , x1 , y1 , x2 , y2 ) => { let next_exists = false ; do { if ( ! node . length ) { if ( node . data && ! node . data . already_flagged ) { const is_overlapping = overlaps ( { left , right , top , bottom } , node . data ) ; if ( is_overlapping ) { node . data . already_flagged = true ; ret . push ( node . data ) ; } } } next_exists = node = node . next ; } while ( next_exists ) ; return x1 >= right || y1 >= bottom || x2 < left || y2 < top ; } ) ; return ret ; function overlaps ( r1 , point ) { return ( point [ 0 ] < r1 . right || point [ 0 ] > r1 . left ) && ( point [ 1 ] < r1 . bottom || point [ 1 ] > r1 . top ) ; } } getTreeRects ( ) { const ret = [ ] ; this . _tree . visit ( ( node , x1 , y1 , x2 , y2 ) => { ret . push ( [ x1 , y1 , x2 - x1 , y2 - y1 ] ) ; } ) ; return ret ; } } export class CanvasPins extends CanvasWidget { _ctx ; _quadtree_rect_arr ; _drawData ; _overlap_count = 0 ; _sub_overlap_count = 0 ; constructor ( ) { super ( ) ; } enter ( domNode , element ) { super . enter . apply ( this , arguments ) ; this . resize ( this . _size ) ; } update ( domNode , element ) { super . update . apply ( this , arguments ) ; this . _ctx = this . element ( ) . node ( ) . getContext ( "2d" ) ; const needs_data_skew = this . topLeftX_exists ( ) && this . topLeftY_exists ( ) && this . bottomRightX_exists ( ) && this . bottomRightY_exists ( ) ; const _data = ( needs_data_skew ? this . skewedData ( ) : this . data ( ) ) . map ( row => [ ... row ] ) . map ( row => { row . is_cluster = false ; row . already_flagged = false ; row . weight = row [ 2 ] ; row . fillStyle = "#FFFFFF" ; row . strokeStyle = "#000000" ; row . overlap_arr = [ ] ; return row ; } ) ; this . _ctx . clearRect ( 0 , 0 , this . width ( ) , this . height ( ) ) ; this . _drawData = this . enableClustering ( ) ? this . applyClustering ( _data ) : _data ; this . draw ( this . _drawData ) ; } applyClustering ( _data ) { const context = this ; this . _overlap_count = 0 ; const arrow_height = 8 ; const pin_h = this . pinHeight ( ) ; const pin_w = this . pinWidth ( ) ; const half_w = pin_w / 2 ; const half_h = pin_h / 2 ; const qt = new Quadtree ( [ [ 0 , 0 ] , [ this . size ( ) . width , this . size ( ) . height ] ] , _data , pin_h , pin_w ) ; this . _quadtree_rect_arr = qt . getTreeRects ( ) ; switch ( this . clusterMode ( ) ) { case "default" : const defData = _data . map ( row => { if ( ! row . already_flagged ) { const mult = this . searchRectMult ( ) ; const left = row [ 0 ] - half_w * mult ; const top = row [ 1 ] - half_h * mult - arrow_height ; const right = row [ 0 ] + half_w * mult ; const bottom = row [ 1 ] + half_h * mult - arrow_height ; row . overlap_arr = qt . searchRect ( left , top , right , bottom ) . filter ( n => n !== row ) ; if ( row . overlap_arr . length === 0 ) { row . already_flagged = false ; } } return row ; } ) ; defData . forEach ( data_row => { if ( data_row . already_flagged && data_row . overlap_arr . length ) { defData . push ( cluster_arr ( [ data_row , ... data_row . overlap_arr ] ) ) ; } } ) ; return defData ; case "grid" : const gridData = [ ... _data ] ; const grid_cell_w = this . gridCellSize ( ) ; const grid_cell_h = this . gridCellSize ( ) ; const grid_row_count = Math . ceil ( this . size ( ) . width / grid_cell_w ) ; const grid_col_count = Math . ceil ( this . size ( ) . height / grid_cell_h ) ; for ( let _col = 0 ; _col < grid_col_count ; _col ++ ) { for ( let _row = 0 ; _row < grid_row_count ; _row ++ ) { const left = grid_cell_w * _row ; const top = grid_cell_h * _col ; const right = grid_cell_w * ( _row + 1 ) ; const bottom = grid_cell_h * ( _col + 1 ) ; const overlap_arr = qt . searchRect ( left , top , right , bottom ) ; if ( overlap_arr . length > 1 ) { const x = left + ( grid_cell_w / 2 ) ; const y = top + ( grid_cell_h / 2 ) ; gridData . push ( cluster_arr ( overlap_arr , x , y ) ) ; } } } return gridData ; } return _data ; function cluster_arr ( arr , x ? , y ? ) { const arr_weight = arr . reduce ( ( a , b ) => { b . already_flagged = true ; return a + b [ 2 ] ; } , 0 ) ; const _x = typeof x !== "undefined" ? x : context . useAveragePos ( ) ? arr . reduce ( ( a , b ) => a + b [ 0 ] * b [ 2 ] , 0 ) / arr_weight : arr [ 0 ] [ 0 ] ; const _y = typeof y !== "undefined" ? y : context . useAveragePos ( ) ? arr . reduce ( ( a , b ) => a + b [ 1 ] * b [ 2 ] , 0 ) / arr_weight : arr [ 0 ] [ 1 ] ; return { 0 : _x , 1 : _y , weight : arr . reduce ( ( a , b ) => a + b [ 2 ] , 0 ) , is_cluster : true , already_flagged : false , fillStyle : "#FFFFFF" , strokeStyle : "#000000" , overlap_arr : [ ] } ; } } drawQuadtree ( ) { if ( ! this . _quadtree_rect_arr ) return ; this . _ctx . strokeStyle = "#000000" ; this . _quadtree_rect_arr . forEach ( n => { this . _ctx . strokeRect ( n [ 0 ] , n [ 1 ] , n [ 2 ] , n [ 3 ] ) ; } ) ; } draw ( data_arr ) { const context = this ; const ctx = this . _ctx ; if ( this . showQuadtree ( ) ) this . drawQuadtree ( ) ; const arrow_height = this . arrowHeight ( ) ; const arrow_width = this . arrowWidth ( ) ; const pin_h = this . pinHeight ( ) ; const pin_w = this . pinWidth ( ) ; const weight_bonus = this . radiusWeightMult ( ) ; let heaviest_cluster = 0 ; if ( this . useWeightedRadius ( ) ) { data_arr . filter ( n => n . is_cluster ) . forEach ( n => { if ( heaviest_cluster < n . weight ) { heaviest_cluster = n . weight ; } } ) ; } data_arr . filter ( n => ! n . already_flagged ) . sort ( ( a , b ) => a [ 1 ] > b [ 1 ] ? 1 : - 1 ) . forEach ( d => { if ( d . is_cluster || this . allCircles ( ) ) { let _radius = pin_w / 2 ; if ( this . useWeightedRadius ( ) ) { _radius += pin_w * ( weight_bonus * d . weight / heaviest_cluster ) ; } drawCirclePin ( { icon : d . weight , left : Math . floor ( d [ 0 ] - _radius ) + 0.5 , top : Math . floor ( d [ 1 ] - _radius - arrow_height ) + 0.5 , radius : _radius , arrow_height , arrow_width } ) ; } else { drawSquarePin ( { fillStyle : d . fillStyle , strokeStyle : d . strokeStyle , icon : d . weight , left : Math . floor ( d [ 0 ] - ( pin_w / 2 ) ) + 0.5 , top : Math . floor ( d [ 1 ] - pin_h - arrow_height ) + 0.5 , width : pin_w , height : pin_h , arrow_height , arrow_width } ) ; } } ) ; function drawCirclePin ( p ) { p . width = p . radius * 2 ; p . height = p . width ; ctx . fillStyle = "#FFFFFF" ; ctx . strokeStyle = "#000000" ; ctx . beginPath ( ) ; ctx . arc ( p . left + ( p . width / 2 ) , p . top + ( p . height / 2 ) , p . radius , 0 , 2 * Math . PI ) ; ctx . closePath ( ) ; ctx . fill ( ) ; ctx . stroke ( ) ; drawPinText ( p ) ; } function drawSquarePin ( p ) { ctx . fillStyle = p . fillStyle ; ctx . strokeStyle = "#000000" ; ctx . fillRect ( p . left , p . top , p . width , p . height ) ; ctx . strokeRect ( p . left , p . top , p . width , p . height ) ; drawArrow ( p ) ; drawPinText ( p ) ; } function drawArrow ( p , is_offset ? ) { const a_x0 = p . left + ( p . width / 2 ) - ( p . arrow_width / 2 ) ; const a_x1 = p . left + ( p . width / 2 ) ; const a_x2 = p . left + ( p . width / 2 ) + ( p . arrow_width / 2 ) ; let a_y0 = p . top + p . height ; let a_y1 = a_y0 + p . arrow_height ; let a_y2 = a_y0 ; if ( ! is_offset ) { ctx . fillStyle = "#FFFFFF" ; ctx . strokeStyle = "#000000" ; } else { ctx . fillStyle = "#FFFFFF" ; ctx . strokeStyle = "#FFFFFF" ; a_y0 -= 2 ; a_y1 -= 2 ; a_y2 -= 2 ; } ctx . beginPath ( ) ; ctx . moveTo ( a_x0 , a_y0 ) ; ctx . lineTo ( a_x1 , a_y1 ) ; ctx . lineTo ( a_x2 , a_y2 ) ; ctx . lineTo ( a_x0 , a_y0 ) ; ctx . closePath ( ) ; ctx . stroke ( ) ; ctx . fill ( ) ; if ( ! is_offset ) drawArrow ( p , true ) ; } function drawPinText ( p ) { ctx . font = `${ context . pinFontSize ( ) }px '${ context . pinFontFamily ( ) }'` ; const x = p . left + ( p . width / 2 ) ; const y = p . top + ( p . height / 2 ) ; ctx . textAlign = "center" ; ctx . textBaseline = "middle" ; ctx . fillStyle = "#000000" ; let size_dec = 0 ; let txt_w = ctx . measureText ( p . icon ) . width ; while ( txt_w > p . width || ! context . shrinkFontToPin ( ) ) { size_dec ++ ; ctx . font = `${ context . pinFontSize ( ) - size_dec }px '${ context . pinFontFamily ( ) }'` ; txt_w = ctx . measureText ( p . icon ) . width ; } ctx . fillText ( p . icon , x , y ) ; } } skewedData ( ) { const context = this ; const retArr = [ ] ; const arr = this . data ( ) ; const box = this . size ( ) ; const coordsWidth = this . bottomRightX ( ) - this . topLeftX ( ) ; const coordsHeight = this . bottomRightY ( ) - this . topLeftY ( ) ; const pixelValueX = coordsWidth / box . width ; const pixelValueY = coordsHeight / box . height ; arr . forEach ( function ( n ) { const left = Math . abs ( n [ 0 ] - context . topLeftX ( ) ) ; const top = Math . abs ( n [ 1 ] - context . topLeftY ( ) ) ; const newX = left / pixelValueX ; const newY = top / pixelValueY ; retArr . push ( [ newX , newY , n [ 2 ] ] ) ; } ) ; return retArr ; } } CanvasPins . prototype . _class += " map_CanvasPins" ; export interface CanvasPins { clusterMode ( ) : any ; clusterMode ( _ ) : CanvasPins ; allCircles ( ) : boolean ; allCircles ( _ ) : CanvasPins ; showQuadtree ( ) : boolean ; showQuadtree ( _ ) : CanvasPins ; useAveragePos ( ) : boolean ; useAveragePos ( _ ) : CanvasPins ; useWeightedRadius ( ) : boolean ; useWeightedRadius ( _ ) : CanvasPins ; radiusWeightMult ( ) : number ; radiusWeightMult ( _ ) : CanvasPins ; shrinkFontToPin ( ) : boolean ; shrinkFontToPin ( _ ) : CanvasPins ; enableClustering ( ) : boolean ; enableClustering ( _ ) : CanvasPins ; searchRectMult ( ) : number ; searchRectMult ( _ ) : CanvasPins ; gridCellSize ( ) : number ; gridCellSize ( _ ) : CanvasPins ; arrowHeight ( ) : number ; arrowHeight ( _ ) : CanvasPins ; arrowWidth ( ) : number ; arrowWidth ( _ ) : CanvasPins ; bottomRightX ( ) : number ; bottomRightX ( _ ) : CanvasPins ; bottomRightY ( ) : number ; bottomRightY ( _ ) : CanvasPins ; topLeftX ( ) : number ; topLeftX ( _ ) : CanvasPins ; topLeftY ( ) : number ; topLeftY ( _ ) : CanvasPins ; pinHeight ( ) : number ; pinHeight ( _ ) : CanvasPins ; pinWidth ( ) : number ; pinWidth ( _ ) : CanvasPins ; pinFontFamily ( ) : string ; pinFontFamily ( _ ) : CanvasPins ; pinFontSize ( ) : number ; pinFontSize ( _ ) : CanvasPins ; bottomRightX_exists ( ) : boolean ; bottomRightY_exists ( ) : boolean ; topLeftX_exists ( ) : boolean ; topLeftY_exists ( ) : boolean ; } CanvasPins . prototype . publish ( "clusterMode" , "default" , "set" , "clusterMode" , [ "defualt" , "grid" ] , { tags : [ "Basic" ] , optional : true } ) ; CanvasPins . prototype . publish ( "gridCellSize" , 80 , "number" , "gridCellSize" , null , { tags : [ "Basic" ] , optional : true } ) ; CanvasPins . prototype . publish ( "allCircles" , false , "boolean" , "allCircles" , null , { tags : [ "Basic" ] , optional : true } ) ; CanvasPins . prototype . publish ( "showQuadtree" , false , "boolean" , "showQuadtree" , null , { tags : [ "Basic" ] , optional : true } ) ; CanvasPins . prototype . publish ( "useAveragePos" , false , "boolean" , "useAveragePos" , null , { tags : [ "Basic" ] , optional : true } ) ; CanvasPins . prototype . publish ( "shrinkFontToPin" , true , "boolean" , "shrinkFontToPin" , null , { tags : [ "Basic" ] , optional : true } ) ; CanvasPins . prototype . publish ( "enableClustering" , true , "boolean" , "enableClustering" , null , { tags : [ "Basic" ] , optional : true } ) ; CanvasPins . prototype . publish ( "useWeightedRadius" , false , "boolean" , "useWeightedRadius" , null , { tags : [ "Basic" ] , optional : true } ) ; CanvasPins . prototype . publish ( "radiusWeightMult" , 0.5 , "number" , "radiusWeightMult" , null , { tags : [ "Basic" ] , optional : true } ) ; CanvasPins . prototype . publish ( "searchRectMult" , 3 , "number" , "searchRectMult" , null , { tags : [ "Basic" ] , optional : true } ) ; CanvasPins . prototype . publish ( "bottomRightX" , null , "number" , "Bottom right x-value" , null , { tags : [ "Basic" ] , optional : true } ) ; CanvasPins . prototype . publish ( "bottomRightY" , null , "number" , "Bottom right y-value" , null , { tags : [ "Basic" ] , optional : true } ) ; CanvasPins . prototype . publish ( "topLeftX" , null , "number" , "Top left x-value" , null , { tags : [ "Basic" ] , optional : true } ) ; CanvasPins . prototype . publish ( "topLeftY" , null , "number" , "Top left y-value" , null , { tags : [ "Basic" ] , optional : true } ) ; CanvasPins . prototype . publish ( "pinHeight" , 20 , "number" , "pinHeight" , null , { tags : [ "Basic" ] , optional : true } ) ; CanvasPins . prototype . publish ( "pinWidth" , 20 , "number" , "pinWidth" , null , { tags : [ "Basic" ] , optional : true } ) ; CanvasPins . prototype . publish ( "pinFontFamily" , "Arial" , "string" , "pinFontFamily" , null , { tags : [ "Basic" ] , optional : true } ) ; CanvasPins . prototype . publish ( "pinFontSize" , 14 , "number" , "pinFontSize" , null , { tags : [ "Basic" ] , optional : true } ) ; CanvasPins . prototype . publish ( "arrowHeight" , 8 , "number" , "arrowHeight" , null , { tags : [ "Basic" ] , optional : true } ) ; CanvasPins . prototype . publish ( "arrowWidth" , 8 , "number" , "arrowWidth" , null , { tags : [ "Basic" ] , optional : true } ) ;