Skip to content

Instantly share code, notes, and snippets.

@derickson
Created January 10, 2012 05:47
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save derickson/1587264 to your computer and use it in GitHub Desktop.
Save derickson/1587264 to your computer and use it in GitHub Desktop.
XQuery Choropleth complete
xquery version "1.0-ml";
import module namespace search="http://marklogic.com/appservices/search"
at "/MarkLogic/appservices/search/search.xqy";
declare namespace kml = "http://www.opengis.net/kml/2.2";
(:White to red color scale in BBGGRR format :)
declare variable $COLOR_SCALE :=
(
"CCFFFF",
"A0EDFF",
"76D9FE",
"4CB2FE",
"3C8DFD",
"2A4EFC",
"1C1AE3",
"2600BD",
"2600BD"
);
(:~
Converts number to single digit hex
I'm sure there is a better way to do this in XQuery, but I am lazy
:)
declare function local:numToHex($n) as xs:string {
if($n gt 15) then 'f'
else if($n gt 9) then
if($n eq 10) then 'a'
else if($n eq 11) then 'b'
else if($n eq 12) then 'c'
else if($n eq 13) then 'd'
else if($n eq 14) then 'e'
else if($n eq 15) then 'f' else '0'
else xs:string($n)
};
(: takes from 1.0 to 0.0 :)
declare function local:numToHexPair($num) {
let $intalpha := fn:round(255 * $num)
let $big := $intalpha idiv 16
let $small := $intalpha mod 16
let $bigchar := local:numToHex($big)
let $smallchar := local:numToHex($small)
return
fn:concat($bigchar,$smallchar)
};
(: Make sure to specify coordinates in CCW order so that KML lighting works correctly :)
declare function local:coord-from-box($box as cts:box, $alt as xs:double){
<kml:coordinates>
{
let $south := xs:string(cts:box-south($box))
let $west := xs:string(cts:box-west($box))
let $north := xs:string(cts:box-north($box))
let $east := xs:string(cts:box-east($box))
let $alt := xs:string($alt)
return
(
fn:string-join((
fn:string-join(($east,$south,$alt),","),
fn:string-join(($east,$north,$alt),","),
fn:string-join(($west,$north,$alt),","),
fn:string-join(($west,$south,$alt),","),
fn:string-join(($east,$south,$alt),",")
)," ")
)
}
</kml:coordinates>
};
(: Color is specified in octal of hex pairs representing transparency
alpha and color triple AABBGGRR example: 88ff0000 :)
declare function local:html-color-from-percentage($freq-prec, $alpha) {
let $colornum := xs:integer( fn:ceiling($freq-prec * fn:count($COLOR_SCALE)) )
let $colornum := if($colornum eq 0) then 1 else $colornum
let $color := $COLOR_SCALE[$colornum]
return
fn:concat(
local:numToHexPair($alpha),
$color
)
};
(: variables that will track maximum values :)
let $maxfreq := 0
let $maxregion := ()
(: analytics bounds -- set to entire world :)
let $lat1 := -90
let $lat2 := 90
let $lon1 := -180
let $lon2 := 180
let $count := 80
(: attempt to make the buckets square :)
let $distx := ($lon2 - $lon1) (: cts:distance( cts:point($lat1,$lon1), cts:point($lat1, $lon2) ) :)
let $disty := ($lat2 - $lat1) (: cts:distance( cts:point($lat1,$lon1), cts:point($lat2, $lon1) ) :)
let $mindist := fn:min(($distx,$disty))
let $sidedist := $mindist div $count
let $_ := xdmp:log(text{"sidedist",$sidedist},"error")
let $countx :=
if( fn:ceiling($distx div $sidedist) castable as xs:integer ) then
xs:integer(fn:ceiling($distx div $sidedist))
else
$count * 2
let $county :=
if( fn:ceiling($disty div $sidedist) castable as xs:integer ) then
xs:integer(fn:ceiling($disty div $sidedist))
else
$count
let $searchres :=
search:search(
"",
<options xmlns="http://marklogic.com/appservices/search">
<additional-query>
{cts:collection-query("event")}
</additional-query>
<constraint name="mygeo">
<geo-elem>
<heatmap s="{$lat1}" w="{$lon1}" n="{$lat2}" e="{$lon2}" latdivs="{$county}" londivs="{$countx}"/>
<facet-option>gridded</facet-option>
<element ns="" name="point"/>
</geo-elem>
</constraint>
<return-results>false</return-results>
<return-facets>true</return-facets>
</options>
)
let $boxes :=
for $box in $searchres//search:box
let $s := xs:float($box/@s)
let $w := xs:float($box/@w)
let $n := xs:float($box/@n)
let $e := xs:float($box/@e)
return
if( ($s ge $lat1) and
($n le $lat2) and
($w ge $lon1) and
($e le $lon2) ) then
let $_ := xdmp:set( $maxfreq, fn:max( ($maxfreq, xs:integer( $box/@count )) ) )
return
$box
else
(: remove this box, because it is accounting for hits outside the search area :)
()
let $stylemap := map:map()
let $markers :=
for $box at $x in $boxes
let $s := xs:float($box/@s)
let $w := xs:float($box/@w)
let $n := xs:float($box/@n)
let $e := xs:float($box/@e)
let $freq := xs:integer($box/@count)
return
let $_ := if($freq eq $maxfreq) then xdmp:set($maxregion, $box) else ()
let $alpha := if ($freq ge 1) then 0.5 else 0.1
let $freq-prec := xs:double(fn:substring( xs:string($freq div $maxfreq), 1 , 5))
let $color-prec := xs:double(fn:substring( xs:string((if($freq eq 0) then 1 else $freq) div $maxfreq), 1 , 5))
let $style-name := fn:concat("s",fn:replace(xs:string($freq-prec),"\.",""))
let $_ := if(map:get($stylemap,$style-name)) then () else map:put($stylemap, $style-name,
<Style id="{$style-name}" xmlns="http://www.opengis.net/kml/2.2">
<PolyStyle>
<color>{local:html-color-from-percentage($color-prec, $alpha)}</color>
<colorMode>normal</colorMode>
</PolyStyle>
</Style>
)
let $alt := (200000 + (800000 * $freq-prec)) * (xs:float($sidedist) div xs:float(9.0))
let $ctsbox := cts:box($s,$w,$n,$e)
return
<Placemark xmlns="http://www.opengis.net/kml/2.2">
<description>
<h3>{$freq} document{if($freq gt 1) then 's' else ()} in this region.</h3>
</description>
<styleUrl>#{$style-name}</styleUrl>
<Polygon xmlns="http://www.opengis.net/kml/2.2">
<extrude>1</extrude>
<altitudeMode>relativeToGround</altitudeMode>
<outerBoundaryIs>
<LinearRing>
{local:coord-from-box($ctsbox,$alt)}
</LinearRing>
</outerBoundaryIs>
</Polygon>
</Placemark>
let $camera := if($boxes) then
<LookAt id="camera1">
<longitude>{fn:avg((xs:float($maxregion/@w), xs:float($maxregion/@e)))}</longitude>
<latitude>{fn:avg((xs:float($maxregion/@n), xs:float($maxregion/@s)))}</latitude>
<altitude>0</altitude>
<altitudeMode>relativeToGround</altitudeMode>
<heading>-10</heading>
<tilt>45</tilt>
<roll>0</roll>
<range>{8000000 * (xs:float($sidedist) div xs:float(9.0)) }</range>
</LookAt>
else ()
return (
xdmp:set-response-content-type("application/vnd.google-earth.kml+xml"),
'<?xml version="1.0" encoding="UTF-8"?>',
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<name>KML Heatmap Global</name>
<open>1</open>
{$camera}
{
<Style id="ml">
<IconStyle>
<color>FF3122D9</color>
</IconStyle>
</Style>,
for $key in map:keys($stylemap)
return
map:get($stylemap,$key),
$markers
}
</Document>
</kml>
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment