diff --git a/DESCRIPTION b/DESCRIPTION index 1665f752..14a00fca 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -21,7 +21,9 @@ Imports: magrittr Suggests: sf, - sp + sp, + testthat (>= 3.0.0) URL: https://github.com/trafficonese/leaflet.extras2 BugReports: https://github.com/trafficonese/leaflet.extras2/issues RoxygenNote: 7.3.1 +Config/testthat/edition: 3 diff --git a/NAMESPACE b/NAMESPACE index a75f5843..e768508e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -36,6 +36,8 @@ export(removeReachability) export(removeSidebar) export(removeSidebyside) export(removeVelocity) +export(setBuildingData) +export(setBuildingStyle) export(showHexbin) export(sidebar_pane) export(sidebar_tabs) diff --git a/R/buildings.R b/R/buildings.R index ef165bbf..ce495f8d 100644 --- a/R/buildings.R +++ b/R/buildings.R @@ -5,49 +5,85 @@ buildingsDependency <- function() { src = system.file("htmlwidgets/lfx-building", package = "leaflet.extras2"), stylesheet = "osm-buildings.css", script = c( - # "osm-buildings.js", - "OSMBuildings.js", + "osm-buildings.js", "osm-buildings-bindings.js") ) ) } -#' Add OSM-Buildings +#' Add OSM-Buildings to a Leaflet Map #' -#' @param map A map widget object created from \code{\link[leaflet]{leaflet}} -#' @param options List of further options. See \code{\link{hexbinOptions}} +#' This function adds 2.5D buildings to a Leaflet map using the OSM Buildings plugin. #' -#' @note Out of the box a legend image is only available for Pressure, -#' Precipitation Classic, Clouds Classic, Rain Classic, Snow, Temperature and -#' Wind Speed. -#' @seealso https://osmbuildings.org/documentation/viewer/ +#' @param map A map widget object created from \code{\link[leaflet]{leaflet}}. +#' @param buildingURL The URL template for the building data. Default is the OSM Buildings tile server: \cr +#' \code{"https://{s}.data.osmbuildings.org/0.2/59fcc2e8/tile/{z}/{x}/{y}.json"}. +#' @param group The name of the group the buildings will be added to. +#' @param eachFn A JavaScript function (using \code{\link[htmlwidgets]{JS}}) that will be called for each building feature. Use this to apply custom logic to each feature. +#' @param clickFn A JavaScript function (using \code{\link[htmlwidgets]{JS}}) that will be called when a building is clicked. Use this to handle click events on buildings. +#' @param data A GeoJSON object containing Polygon features representing the buildings. The properties of these polygons can include attributes like \code{height}, \code{color}, \code{roofColor}, and others as specified in the OSM Buildings documentation. +#' +#' @details +#' The `data` parameter allows you to provide custom building data as a GeoJSON object. The following properties can be used within the GeoJSON: +#' \itemize{ +#' \item \strong{height} +#' \item \strong{minHeight} +#' \item \strong{color/wallColor} +#' \item \strong{material} +#' \item \strong{roofColor} +#' \item \strong{roofMaterial} +#' \item \strong{shape} +#' \item \strong{roofShape} +#' \item \strong{roofHeight} +#' } +#' +#' See the OSM Wiki: \href{https://wiki.openstreetmap.org/wiki/Simple_3D_Buildings} +#' +#' @seealso \url{https://github.com/kekscom/osmbuildings/} for more details on the OSM Buildings plugin and available properties. #' @family OSM-Buildings Plugin #' @export addBuildings <- function( - map, layerId = NULL, group = NULL, opacity = 0.5, - attribution = '© Map tiles Mapbox') { - - # if (is.null(apikey)) { - # apikey <- Sys.getenv("MAPBOX") - # if (apikey == "") { - # stop("You must either pass an `apikey` directly or save it as ", - # "system variable under `MAPBOX`.") - # } - # } + map, + buildingURL = "https://{s}.data.osmbuildings.org/0.2/59fcc2e8/tile/{z}/{x}/{y}.json", + group = NULL, + eachFn = NULL, clickFn = NULL, data = NULL) { map$dependencies <- c(map$dependencies, buildingsDependency()) - invokeMethod(map, getMapData(map), "addBuilding", layerId, group, - opacity, attribution) + invokeMethod(map, getMapData(map), "addBuilding", + buildingURL, group, + eachFn, clickFn, data) } #' Update the Shadows OSM-Buildings with a POSIXct timestamp #' -#' @param map A map widget object created from \code{\link[leaflet]{leaflet}} -#' @seealso https://osmbuildings.org/documentation/viewer/ +#' @inheritParams addBuildings +#' @param time a timestamp that can be converted to POSIXct #' @family OSM-Buildings Plugin #' @export updateBuildingTime <- function(map, time) { - invokeMethod(map, NULL, "updateBuildingTime", as.POSIXct(time)) + invokeMethod(map, NULL, "updateBuildingTime", time) +} + +#' Update the OSM-Buildings Style +#' +#' @inheritParams addBuildings +#' @param style A named list of styles +#' @family OSM-Buildings Plugin +#' @export +setBuildingStyle <- function(map, style = list(color = "#ffcc00", + wallColor = "#ffcc00", + roofColor = "orange", + shadows = TRUE)) { + invokeMethod(map, NULL, "setBuildingStyle", style) +} + +#' Update the OSM-Buildings Data +#' +#' @inheritParams addBuildings +#' @family OSM-Buildings Plugin +#' @export +setBuildingData <- function(map, data) { + invokeMethod(map, NULL, "setBuildingData", data) } diff --git a/inst/examples/Buildings_mini.geojson b/inst/examples/Buildings_mini.geojson new file mode 100644 index 00000000..ad4a4c30 --- /dev/null +++ b/inst/examples/Buildings_mini.geojson @@ -0,0 +1 @@ +{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"Shape__Area":176.63671875},"geometry":{"type":"Polygon","coordinates":[[[-76.4804996045569,42.4337884600754],[-76.4805379680812,42.4338087538101],[-76.4805187879662,42.4338284557592],[-76.4805509401373,42.4338455919873],[-76.4804662851554,42.4339336674861],[-76.4803956694538,42.4338964165343],[-76.4804996045569,42.4337884600754]]]}},{"type":"Feature","properties":{"Shape__Area":345.98828125},"geometry":{"type":"Polygon","coordinates":[[[-76.4917147984674,42.437699019829],[-76.4917157586844,42.4377966250581],[-76.4917498459528,42.4377962921787],[-76.4917500189652,42.4378152011893],[-76.4917731792594,42.4378150396315],[-76.4917736193283,42.4378567289911],[-76.4917635795041,42.4378669861224],[-76.4917384137284,42.4378671460758],[-76.4917381362015,42.4378515689695],[-76.4917055523917,42.4378519030471],[-76.4916990310511,42.4378547787747],[-76.4916590285082,42.4378549268497],[-76.4916520236004,42.437846007438],[-76.491607409454,42.4378463318654],[-76.4916069674922,42.4378051826799],[-76.4915981458976,42.4378051756207],[-76.491596895207,42.437700185948],[-76.4917147984674,42.437699019829]]]}},{"type":"Feature","properties":{"Shape__Area":267.796875},"geometry":{"type":"Polygon","coordinates":[[[-76.5035337738459,42.435060557038],[-76.5034652989897,42.4350627924973],[-76.5034672332501,42.435094871332],[-76.5034455827945,42.4350955791824],[-76.503445912491,42.4351010622047],[-76.5033311542365,42.4351046265652],[-76.5033308344195,42.4350990283119],[-76.5033106802032,42.4350996498525],[-76.5033089967195,42.4350701784426],[-76.5033291084123,42.4350695379664],[-76.5033281113062,42.4350517717624],[-76.5033631216726,42.4350506852281],[-76.5033611850748,42.4350185757797],[-76.5033864087956,42.4350178056161],[-76.5033860950374,42.4350122181705],[-76.5035307575087,42.4350076842528],[-76.5035310657796,42.4350118591322],[-76.5035523405363,42.4350112437345],[-76.5035548324292,42.4350550339849],[-76.5035335016566,42.4350557231682],[-76.5035337738459,42.435060557038]]]}},{"type":"Feature","properties":{"Shape__Area":127.296875},"geometry":{"type":"Polygon","coordinates":[[[-76.4759524752853,42.4385379409239],[-76.4759592414087,42.4386270880435],[-76.4758745174658,42.4386306107282],[-76.4758676517972,42.4385414626099],[-76.4759524752853,42.4385379409239]]]}},{"type":"Feature","properties":{"Shape__Area":63.15234375},"geometry":{"type":"Polygon","coordinates":[[[-76.4754381677613,42.4318082068041],[-76.4754809329536,42.4318319275006],[-76.4754236956695,42.4318883302206],[-76.475381130989,42.4318646087915],[-76.4754381677613,42.4318082068041]]]}},{"type":"Feature","properties":{"Shape__Area":804.28515625},"geometry":{"type":"Polygon","coordinates":[[[-76.4933630558631,42.4419432576379],[-76.4933692794826,42.4421487364427],[-76.4933775947121,42.4421530652759],[-76.4933782705625,42.4421708934121],[-76.493359693205,42.4421908680421],[-76.4933323214576,42.442191386731],[-76.4932946494504,42.4421723591604],[-76.4932941754185,42.4421528197248],[-76.4930865251091,42.4421562576079],[-76.4930851801135,42.4421151977773],[-76.4930629211068,42.4421155403683],[-76.4930613919052,42.4420639460612],[-76.4931591505082,42.442062491605],[-76.493158476476,42.442042591894],[-76.4931673001391,42.4420424187761],[-76.493154275754,42.4420357454607],[-76.4931689368898,42.4420199100322],[-76.4931975913848,42.442034339059],[-76.4931958657208,42.4419789624667],[-76.4932659513436,42.4419778471739],[-76.4932655718912,42.4419627192486],[-76.4933062802541,42.4419621210212],[-76.4933056033396,42.4419442028537],[-76.4933630558631,42.4419432576379]]]}},{"type":"Feature","properties":{"Shape__Area":199.39453125},"geometry":{"type":"Polygon","coordinates":[[[-76.4787534084869,42.4409282484676],[-76.4787564658364,42.441018653313],[-76.4787448357081,42.4410188227611],[-76.4787455054505,42.4410378222625],[-76.4786649935411,42.4410392792755],[-76.4786643218583,42.4410207299185],[-76.4786422634225,42.4410212499511],[-76.4786392072846,42.4409302157987],[-76.4787534084869,42.4409282484676]]]}},{"type":"Feature","properties":{"Shape__Area":253.99609375},"geometry":{"type":"Polygon","coordinates":[[[-76.502347211727,42.4329232099141],[-76.5023493916232,42.4330217162944],[-76.5023405694816,42.4330217100617],[-76.5023412387351,42.4330465622682],[-76.5023100611824,42.4330469003542],[-76.5023095858613,42.4330271808645],[-76.5022996600756,42.4330272638781],[-76.5023000358518,42.433046893268],[-76.5022326685716,42.4330476558929],[-76.5022324954365,42.4330264061157],[-76.5022238738251,42.4330264000159],[-76.5022223644313,42.4329528349709],[-76.5022129404753,42.4329530083615],[-76.5022124760996,42.4329248252086],[-76.502347211727,42.4329232099141]]]}},{"type":"Feature","properties":{"Shape__Area":2687.15625},"geometry":{"type":"Polygon","coordinates":[[[-76.4858391006211,42.4359088550454],[-76.4857900357414,42.4358590756544],[-76.4857386961732,42.4358105727232],[-76.4856851389539,42.4357634030158],[-76.4856294284142,42.4357176224023],[-76.4855716313175,42.4356732849542],[-76.4855118132154,42.4356304420409],[-76.4854500469526,42.4355891441381],[-76.485386404159,42.4355494408201],[-76.4853209601144,42.4355113789633],[-76.4852537913189,42.4354750018444],[-76.4851849779165,42.4354403536434],[-76.4852414505764,42.4353742753687],[-76.4852158878537,42.4353622876811],[-76.4852569018754,42.4353250974117],[-76.485268131037,42.4353101387261],[-76.4852882151555,42.4353195576785],[-76.4853075348813,42.4352969516067],[-76.485379670956,42.4353332240042],[-76.4854501636469,42.4353712298199],[-76.4855189352367,42.4354109275769],[-76.4855859128702,42.4354522749024],[-76.4856510224806,42.4354952267215],[-76.4857141960773,42.435539737965],[-76.4857753656758,42.4355857590618],[-76.4858344645039,42.4356332422428],[-76.4858914294416,42.4356821352403],[-76.485946198585,42.4357323848877],[-76.4859987136745,42.4357839389215],[-76.4858724782898,42.4358536350754],[-76.4859073465379,42.4358882873278],[-76.4858813573948,42.4359026366327],[-76.4858891057631,42.4359103371334],[-76.4858683132227,42.4359218171136],[-76.4858494962843,42.435903115508],[-76.4858391006211,42.4359088550454]]]}},{"type":"Feature","properties":{"Shape__Area":175.1875},"geometry":{"type":"Polygon","coordinates":[[[-76.5046171160388,42.4445603064262],[-76.5046141072987,42.4445604844187],[-76.5046125265181,42.4445424747754],[-76.5046836173296,42.4445406329585],[-76.5046842029275,42.4445535093485],[-76.5046868105318,42.444553331079],[-76.5046878795991,42.444580074111],[-76.504709438602,42.4445792786365],[-76.5047123586526,42.4446490623367],[-76.5047017309835,42.4446490550472],[-76.504702608273,42.4446687747627],[-76.5046475567434,42.4446725191174],[-76.5046207246047,42.4446412560335],[-76.5046171160388,42.4445603064262]]]}},{"type":"Feature","properties":{"Shape__Area":361.671875},"geometry":{"type":"Polygon","coordinates":[[[-76.4787664531785,42.4360974471362],[-76.4787667275037,42.4361137445105],[-76.478836203562,42.4361136278054],[-76.4788364789675,42.4361885433297],[-76.4787807380484,42.4361886725631],[-76.4787807254763,42.4361962359224],[-76.4787425288341,42.4361963811372],[-76.4787426997305,42.4362142089169],[-76.4787197405384,42.4362143680257],[-76.4787197808222,42.4361901474503],[-76.4785839372267,42.4361905635766],[-76.4785835637769,42.4361133972268],[-76.4786761983154,42.4361131217193],[-76.4786759237309,42.4360977246386],[-76.4787664531785,42.4360974471362]]]}},{"type":"Feature","properties":{"Shape__Area":266.64453125},"geometry":{"type":"Polygon","coordinates":[[[-76.492889545482,42.4359950172782],[-76.4928904260726,42.4360097845978],[-76.4929335368892,42.4360084681616],[-76.4929368593737,42.436068798593],[-76.4929287378322,42.4360689722472],[-76.4929312836472,42.43611156357],[-76.492784505336,42.4361162201839],[-76.4927814679505,42.4360673255101],[-76.4928019203423,42.4360667114582],[-76.4927980097654,42.4359979159926],[-76.492889545482,42.4359950172782]]]}},{"type":"Feature","properties":{"Shape__Area":225.4296875},"geometry":{"type":"Polygon","coordinates":[[[-76.4855454154509,42.4546629304284],[-76.4855500292904,42.4546621241023],[-76.4855889591292,42.4547785802619],[-76.48550188817,42.4547946238953],[-76.4854885123842,42.4547540039442],[-76.4854660437321,42.4547580360733],[-76.485455262456,42.4547259729002],[-76.4854788352866,42.4547217607573],[-76.4854643632435,42.4546778078893],[-76.4854764003052,42.4546755674379],[-76.4854731081173,42.4546645801743],[-76.4854699142222,42.4546544923854],[-76.4854838664153,42.4546465808334],[-76.4854898882375,42.454643344025],[-76.4855366334238,42.4546351903658],[-76.4855454154509,42.4546629304284]]]}},{"type":"Feature","properties":{"Shape__Area":1277.921875},"geometry":{"type":"Polygon","coordinates":[[[-76.5077166801357,42.4314492344605],[-76.5073318212686,42.4314620000178],[-76.5073198920808,42.4312646222522],[-76.5077047497427,42.4312518567335],[-76.5077166801357,42.4314492344605]]]}},{"type":"Feature","properties":{"Shape__Area":291.640625},"geometry":{"type":"Polygon","coordinates":[[[-76.4948586428299,42.4441071255128],[-76.4948591371261,42.444112617681],[-76.4949084710594,42.4441104050148],[-76.4949104410154,42.444135798395],[-76.4950049976599,42.4441315489878],[-76.4950099269527,42.4441919705411],[-76.4947853148271,42.4442018823382],[-76.4947778229713,42.4441106642775],[-76.4948586428299,42.4441071255128]]]}},{"type":"Feature","properties":{"Shape__Area":196.15234375},"geometry":{"type":"Polygon","coordinates":[[[-76.5012518569606,42.4519254851065],[-76.5013112244919,42.452077516641],[-76.5012500379442,42.4520905288724],[-76.5012147566076,42.4520004627335],[-76.5011910837671,42.4520055783349],[-76.5011667971385,42.4519436127595],[-76.5012518569606,42.4519254851065]]]}},{"type":"Feature","properties":{"Shape__Area":274.3359375},"geometry":{"type":"Polygon","coordinates":[[[-76.5128729326646,42.4330934977998],[-76.5128593178432,42.4330766512196],[-76.512924410984,42.4330488685116],[-76.5129415285574,42.4330710298739],[-76.5129887681725,42.4330509795995],[-76.5129649433812,42.4330199003095],[-76.51301980553,42.4329966127398],[-76.5130672556474,42.433057870875],[-76.5130598334943,42.4330611974156],[-76.5130792536023,42.4330860610528],[-76.512982166132,42.4331273304389],[-76.5129653497309,42.4331053502269],[-76.5128569286158,42.4331513841109],[-76.5128319019752,42.4331191345722],[-76.51287783801,42.4330998037777],[-76.5128729326646,42.4330934977998]]]}},{"type":"Feature","properties":{"Shape__Area":1614.12109375},"geometry":{"type":"Polygon","coordinates":[[[-76.5066547319233,42.4462688306652],[-76.5065829021833,42.446182352706],[-76.5065292524807,42.4462067948279],[-76.5063545312796,42.4459964413011],[-76.5064902314624,42.4459346156233],[-76.5065096450377,42.4459579885284],[-76.506635877583,42.4459004768366],[-76.5068598606444,42.4461753732786],[-76.5067336277502,42.4462328861143],[-76.5067083816312,42.4462443875846],[-76.5066547319233,42.4462688306652]]]}},{"type":"Feature","properties":{"Shape__Area":445.71875},"geometry":{"type":"Polygon","coordinates":[[[-76.5240360887441,42.4490086919248],[-76.524023552753,42.4490102168364],[-76.5240259501387,42.4490203922786],[-76.5239782134868,42.449026130341],[-76.5238568682602,42.4490406539789],[-76.5238146336647,42.4488485745267],[-76.5239357793354,42.4488338698709],[-76.5239685289993,42.4489817346199],[-76.5240067374848,42.4489771629107],[-76.5240162657025,42.4489759074325],[-76.5240287007882,42.4489743824695],[-76.5240360887441,42.4490086919248]]]}},{"type":"Feature","properties":{"Shape__Area":295.9921875},"geometry":{"type":"Polygon","coordinates":[[[-76.4970539380426,42.4488135475477],[-76.4970545207572,42.4488274142861],[-76.4970619415409,42.4488270597544],[-76.4970612538494,42.4488161639012],[-76.4970997610872,42.4488149324668],[-76.4971004495454,42.4488243878529],[-76.4971907004508,42.4488215738755],[-76.4971948168207,42.4488906383796],[-76.4970085991923,42.4488968912696],[-76.4970070297547,42.4488721293529],[-76.4969417477618,42.4488743309016],[-76.4969383163932,42.4488174226492],[-76.4970539380426,42.4488135475477]]]}},{"type":"Feature","properties":{"Shape__Area":250.8828125},"geometry":{"type":"Polygon","coordinates":[[[-76.5141100742951,42.4326496022761],[-76.5141085760016,42.4326770207477],[-76.5140345500886,42.4326748018104],[-76.5140360484143,42.4326473833397],[-76.5139250083871,42.4326440552949],[-76.513927105866,42.4326056688974],[-76.5140566510685,42.4326095519027],[-76.5140584483163,42.4325766499168],[-76.5141694870164,42.4325799778315],[-76.5141655937095,42.4326512662219],[-76.5141100742951,42.4326496022761]]]}},{"type":"Feature","properties":{"Shape__Area":47.58984375},"geometry":{"type":"Polygon","coordinates":[[[-76.4827194651053,42.4359675432523],[-76.4827564261739,42.4359882851532],[-76.4827068218217,42.4360371338947],[-76.4826698619562,42.436016391979],[-76.4827194651053,42.4359675432523]]]}},{"type":"Feature","properties":{"Shape__Area":134.4375},"geometry":{"type":"Polygon","coordinates":[[[-76.5206949660285,42.4318772551954],[-76.5207949460262,42.4319450209583],[-76.5207411698055,42.4319885721819],[-76.5206409905743,42.4319207162342],[-76.5206949660285,42.4318772551954]]]}},{"type":"Feature","properties":{"Shape__Area":275.85546875},"geometry":{"type":"Polygon","coordinates":[[[-76.5037622460805,42.4413835965233],[-76.5037585992652,42.4413338914972],[-76.5038539538716,42.4413301755176],[-76.5038598663979,42.4414116669679],[-76.5038800207628,42.4414106005869],[-76.5038817157941,42.4414178050087],[-76.5038948402586,42.4414259176448],[-76.5038960258616,42.441439964843],[-76.5038818785837,42.4414476993617],[-76.5038820696997,42.4414551728291],[-76.5038369491119,42.4414570321633],[-76.503838020726,42.4414814344568],[-76.5037649258345,42.4414848048583],[-76.5037632529171,42.4414601320588],[-76.5037501176367,42.4414605739874],[-76.5037436968661,42.441384123822],[-76.5037622460805,42.4413835965233]]]}},{"type":"Feature","properties":{"Shape__Area":185.96875},"geometry":{"type":"Polygon","coordinates":[[[-76.5002915821999,42.4343135109081],[-76.5002919622306,42.4343294481957],[-76.5003348702596,42.4343288490641],[-76.5003374382312,42.4344339296889],[-76.5002397923992,42.4344354794815],[-76.5002371222885,42.4343314791338],[-76.5002417333782,42.4343314824762],[-76.5002413551441,42.4343141947481],[-76.5002915821999,42.4343135109081]]]}},{"type":"Feature","properties":{"Shape__Area":206.04296875},"geometry":{"type":"Polygon","coordinates":[[[-76.51542310518,42.4387214598557],[-76.5154977215911,42.4387338983793],[-76.5155025958537,42.4387178346208],[-76.5155351251647,42.4387232565273],[-76.5155302509099,42.4387393202872],[-76.5155374791076,42.4387405255541],[-76.515520685117,42.4387958710838],[-76.5155351415262,42.4387982807181],[-76.5155270177566,42.438825054251],[-76.515512560127,42.4388226437147],[-76.5155089044256,42.4388346919838],[-76.5153945303355,42.4388156262571],[-76.51542310518,42.4387214598557]]]}},{"type":"Feature","properties":{"Shape__Area":508.53515625},"geometry":{"type":"Polygon","coordinates":[[[-76.491182855391,42.4418611187597],[-76.4911840373707,42.4418757062609],[-76.4912420931221,42.4418736813408],[-76.4912571167957,42.4418844987287],[-76.4912613275837,42.4418843220522],[-76.491262400653,42.4419044021541],[-76.491273129269,42.4419040506534],[-76.4912895554197,42.4419158594858],[-76.4912906259948,42.4419376501425],[-76.4912741654642,42.4419501528009],[-76.4912637370714,42.4419505045428],[-76.491264717985,42.4419654519987],[-76.4912449440455,42.4419803827833],[-76.4911773630538,42.4419826701355],[-76.4911782478376,42.4419951867231],[-76.4911425524954,42.4419964184318],[-76.4911324148481,42.442004063671],[-76.4911170730835,42.4420045915062],[-76.4911080583685,42.4419982813011],[-76.4910316546426,42.4420009206859],[-76.4910233272454,42.4418683737119],[-76.4910475910778,42.4418676730137],[-76.4910484969768,42.4418657822269],[-76.491182855391,42.4418611187597]]]}},{"type":"Feature","properties":{"Shape__Area":450.80078125},"geometry":{"type":"Polygon","coordinates":[[[-76.5211636357724,42.4318629237444],[-76.5211485739174,42.4318879473736],[-76.521022202618,42.4318463688582],[-76.5210597572475,42.4317836306092],[-76.5210924926246,42.4317287231966],[-76.5211040173514,42.4317325115921],[-76.5211160663878,42.4317124388551],[-76.5211639693283,42.4317283118344],[-76.5211605546774,42.4317340727701],[-76.5211667677472,42.4317361468178],[-76.5212098452001,42.4316641366752],[-76.5212714768116,42.4316844293939],[-76.5212121322272,42.4317837132818],[-76.5212095225902,42.4317877640949],[-76.5212351766513,42.4317964217256],[-76.521208567238,42.4318407981479],[-76.5211822120151,42.4318320492014],[-76.5211636357724,42.4318629237444]]]}},{"type":"Feature","properties":{"Shape__Area":1199.02734375},"geometry":{"type":"Polygon","coordinates":[[[-76.5074596405895,42.4387032294976],[-76.5074762174834,42.439173346847],[-76.5073326430626,42.4391760426592],[-76.5073283739512,42.439058716254],[-76.5073096253367,42.4390588838804],[-76.5073020678644,42.4388415182518],[-76.5073224210271,42.4388411716318],[-76.5073177710823,42.4387061064787],[-76.5074596405895,42.4387032294976]]]}},{"type":"Feature","properties":{"Shape__Area":371.70703125},"geometry":{"type":"Polygon","coordinates":[[[-76.524039402419,42.4494290967578],[-76.5239449234349,42.4494502974433],[-76.5238894994253,42.4493137664252],[-76.5239026385904,42.4493108013737],[-76.5238872322372,42.4492726164975],[-76.5239939479372,42.449248630388],[-76.5240066523302,42.4492799716314],[-76.5240356384022,42.4492735036258],[-76.5240550462884,42.4493212354485],[-76.5240263616786,42.449327703615],[-76.5240401671164,42.449362016388],[-76.5240144919969,42.4493676758453],[-76.524039402419,42.4494290967578]]]}},{"type":"Feature","properties":{"Shape__Area":461.1328125},"geometry":{"type":"Polygon","coordinates":[[[-76.4973156884569,42.4431433917478],[-76.4973179300176,42.4431912953219],[-76.497364453904,42.443190069812],[-76.4973680631504,42.4432636363631],[-76.4973254495895,42.4432648648084],[-76.4973257430533,42.4432698175406],[-76.4971321239327,42.4432749838345],[-76.4971260809856,42.4431485619422],[-76.4973156884569,42.4431433917478]]]}},{"type":"Feature","properties":{"Shape__Area":81.375},"geometry":{"type":"Polygon","coordinates":[[[-76.4848729356669,42.4369763470277],[-76.484914760587,42.4370291473743],[-76.484846641082,42.4370588021726],[-76.4848046157971,42.4370059115999],[-76.4848729356669,42.4369763470277]]]}},{"type":"Feature","properties":{"Shape__Area":150.6328125},"geometry":{"type":"Polygon","coordinates":[[[-76.501458424139,42.4482359221601],[-76.5014988725808,42.4482830426302],[-76.5014098759754,42.4483250281106],[-76.5013427945415,42.448246914008],[-76.5013661722235,42.4482358562318],[-76.5013912562519,42.4482241685657],[-76.5014011678505,42.4482358812503],[-76.5014032705103,42.4482380434546],[-76.5014078866511,42.4482358860524],[-76.5014438064431,42.4482188934939],[-76.501458424139,42.4482359221601]]]}},{"type":"Feature","properties":{"Shape__Area":37.51953125},"geometry":{"type":"Polygon","coordinates":[[[-76.4953988733227,42.4476242849262],[-76.4954043180752,42.447674712676],[-76.4953602954291,42.4476773797762],[-76.4953548507105,42.4476269529247],[-76.4953988733227,42.4476242849262]]]}},{"type":"Feature","properties":{"Shape__Area":500.87890625},"geometry":{"type":"Polygon","coordinates":[[[-76.4816100925764,42.4559046856913],[-76.481623028388,42.4560910817124],[-76.4816035707431,42.4560919656369],[-76.4816044503168,42.4561066429609],[-76.4815546049749,42.4561084893244],[-76.4815537241503,42.4560953424936],[-76.4814717863042,42.4560983315995],[-76.4814692390066,42.4560609618587],[-76.481451688156,42.4560615764643],[-76.4814451213398,42.4559672975573],[-76.4814661822706,42.455966596043],[-76.4814639312408,42.4559324685124],[-76.4814995351326,42.4559311497117],[-76.4814979664817,42.4559088175001],[-76.4816100925764,42.4559046856913]]]}},{"type":"Feature","properties":{"Shape__Area":339.5},"geometry":{"type":"Polygon","coordinates":[[[-76.4951622249539,42.4305365794093],[-76.4951665115315,42.4306257236175],[-76.4950264644905,42.4306293979609],[-76.4950262704136,42.4306248054065],[-76.4949352439387,42.4306272569766],[-76.4949311532281,42.4305425243565],[-76.4951622249539,42.4305365794093]]]}},{"type":"Feature","properties":{"Shape__Area":51.90234375},"geometry":{"type":"Polygon","coordinates":[[[-76.500109774499,42.4513235458361],[-76.5001111270106,42.4513625348532],[-76.5000319055724,42.451364007806],[-76.5000305531087,42.4513250196883],[-76.500109774499,42.4513235458361]]]}},{"type":"Feature","properties":{"Shape__Area":175.33984375},"geometry":{"type":"Polygon","coordinates":[[[-76.5001763487755,42.4538330434631],[-76.5001777084079,42.4538667198481],[-76.5001439123971,42.4538673255301],[-76.5001452697491,42.4539027124673],[-76.500017907468,42.4539053217414],[-76.5000169392098,42.4538791189578],[-76.5000047047254,42.4538794701812],[-76.5000031566434,42.4538366998141],[-76.5001763487755,42.4538330434631]]]}},{"type":"Feature","properties":{"Shape__Area":303.42578125},"geometry":{"type":"Polygon","coordinates":[[[-76.495676549813,42.4427888273297],[-76.4956784915483,42.4428346600352],[-76.4956579362191,42.4428351844913],[-76.4956599815925,42.4428790366313],[-76.4955365508157,42.4428823632755],[-76.4955365558436,42.442878762107],[-76.495483915361,42.4428801622464],[-76.4954281654277,42.4428816500064],[-76.4954249467577,42.4428172675791],[-76.4955356427926,42.4428144714779],[-76.4955352562519,42.4428040259813],[-76.4956088529871,42.442802191711],[-76.4956084664331,42.4427917471149],[-76.495676549813,42.4427888273297]]]}},{"type":"Feature","properties":{"Shape__Area":177.21875},"geometry":{"type":"Polygon","coordinates":[[[-76.4988140132576,42.4501073108812],[-76.498817662108,42.4501529646755],[-76.4987998113441,42.4501537617756],[-76.4988023779128,42.4501842880667],[-76.4987451163408,42.4501868566728],[-76.4987442293354,42.4501756914978],[-76.4986662088713,42.450179235065],[-76.4986659132081,42.4501758137372],[-76.4986383353759,42.4501769637439],[-76.4986347842801,42.4501337408045],[-76.4986597547258,42.4501324088143],[-76.498662963119,42.4501322311259],[-76.4986625674045,42.4501281795198],[-76.4987278516113,42.4501252558595],[-76.4987264661747,42.4501113885403],[-76.4988140132576,42.4501073108812]]]}},{"type":"Feature","properties":{"Shape__Area":53.24609375},"geometry":{"type":"Polygon","coordinates":[[[-76.497508734167,42.436405922941],[-76.4975706203941,42.4364431847151],[-76.4975357460392,42.4364740080965],[-76.4974746422525,42.4364360545637],[-76.497508734167,42.436405922941]]]}},{"type":"Feature","properties":{"Shape__Area":56.96875},"geometry":{"type":"Polygon","coordinates":[[[-76.513021343019,42.4448938802498],[-76.5130319428538,42.4449199088142],[-76.5129128935404,42.4449471182148],[-76.512902094397,42.4449210895171],[-76.513021343019,42.4448938802498]]]}},{"type":"Feature","properties":{"Shape__Area":163.6015625},"geometry":{"type":"Polygon","coordinates":[[[-76.5006424915573,42.4528178060056],[-76.5006451451923,42.4529354012977],[-76.5005622119888,42.4529364217929],[-76.5005598589106,42.4528187366869],[-76.5006424915573,42.4528178060056]]]}},{"type":"Feature","properties":{"Shape__Area":173.72265625},"geometry":{"type":"Polygon","coordinates":[[[-76.4946454047698,42.44934745522],[-76.4946453941005,42.4493550185654],[-76.4947612137795,42.4493537576775],[-76.4947615884344,42.4493725758717],[-76.4947718048244,42.4493803280776],[-76.494771987016,42.4493933842538],[-76.4947620495788,42.449400759871],[-76.4947622239048,42.4494193978519],[-76.4947187039043,42.4494199052924],[-76.4947188897663,42.4494303497242],[-76.4946829909564,42.4494306820838],[-76.4946828045926,42.4494205977684],[-76.4946448002735,42.4494209284876],[-76.4946444242575,42.4494030997131],[-76.4946075224206,42.4494035213023],[-76.49460648049,42.4493596708706],[-76.4946103915029,42.4493594938394],[-76.4946102071851,42.4493479681577],[-76.4946454047698,42.44934745522]]]}},{"type":"Feature","properties":{"Shape__Area":229.1015625},"geometry":{"type":"Polygon","coordinates":[[[-76.5038543430378,42.4313430445788],[-76.5038458211981,42.4313431286996],[-76.5038463685065,42.4313860794489],[-76.5038306300551,42.431386248594],[-76.5038310974221,42.43141263026],[-76.5037391705458,42.4314133776358],[-76.5037388028736,42.4313869951384],[-76.5037188545986,42.4313871613451],[-76.5037175600857,42.4313009004886],[-76.5038142983038,42.4313000673383],[-76.5038142932967,42.4313040286326],[-76.503853690303,42.4313036958362],[-76.5038543430378,42.4313430445788]]]}},{"type":"Feature","properties":{"Shape__Area":261.51953125},"geometry":{"type":"Polygon","coordinates":[[[-76.4789573776346,42.4357708602767],[-76.478931586262,42.4357866846685],[-76.4788951478182,42.4357543264146],[-76.4788872190447,42.4357591816796],[-76.4788550850478,42.4357307886289],[-76.4788632135913,42.4357256643603],[-76.4788586091328,42.4357216079399],[-76.4789538478647,42.435662627298],[-76.4789582516365,42.4356667735613],[-76.4789998998375,42.435640879401],[-76.4790408430755,42.4356772930446],[-76.4790174591078,42.4356918592344],[-76.4790553977534,42.435726919697],[-76.4789670882299,42.4357791536248],[-76.4789573776346,42.4357708602767]]]}},{"type":"Feature","properties":{"Shape__Area":718.62109375},"geometry":{"type":"Polygon","coordinates":[[[-76.517464474647,42.4554843051011],[-76.5175172753939,42.4555809726802],[-76.5174142133289,42.4556118402596],[-76.5174433931017,42.4556652614051],[-76.517314568933,42.4557038466835],[-76.5172853904686,42.455650424606],[-76.5172080937587,42.4556735751425],[-76.5171552946267,42.4555769065231],[-76.517464474647,42.4554843051011]]]}},{"type":"Feature","properties":{"Shape__Area":95.26171875},"geometry":{"type":"Polygon","coordinates":[[[-76.4984061125545,42.4530596421427],[-76.4984064051748,42.4530653150962],[-76.4984373941459,42.4530643477308],[-76.498439648091,42.4531030664354],[-76.4984086834531,42.4531040059104],[-76.4984113565519,42.453150113618],[-76.4983620166185,42.4531516075496],[-76.498356772692,42.4530611360721],[-76.4984061125545,42.4530596421427]]]}},{"type":"Feature","properties":{"Shape__Area":303.2890625},"geometry":{"type":"Polygon","coordinates":[[[-76.4949854628129,42.4443337667218],[-76.4949894407455,42.4444284931995],[-76.4947989270682,42.44443293861],[-76.4947948496199,42.4443383020783],[-76.4949854628129,42.4443337667218]]]}},{"type":"Feature","properties":{"Shape__Area":278.40625},"geometry":{"type":"Polygon","coordinates":[[[-76.4910922911708,42.4559586294125],[-76.4912063418815,42.4560096839064],[-76.4911181330126,42.4561174822445],[-76.4910084926955,42.4560683218273],[-76.4910379289046,42.4560321484325],[-76.4910338199031,42.4560302545129],[-76.4910922911708,42.4559586294125]]]}},{"type":"Feature","properties":{"Shape__Area":188.2578125},"geometry":{"type":"Polygon","coordinates":[[[-76.5077459893097,42.4430307186514],[-76.5077480490469,42.4430686277503],[-76.5077760241838,42.4430679250581],[-76.5077798388936,42.4431465339615],[-76.5077175723106,42.4431482034619],[-76.5077168848054,42.4431366783567],[-76.5076786830253,42.4431376434732],[-76.5076778011163,42.443119994446],[-76.5076719857863,42.4431201706664],[-76.5076697333298,42.4430757793292],[-76.5076591042769,42.4430761324302],[-76.507656850207,42.4430330906306],[-76.5077459893097,42.4430307186514]]]}},{"type":"Feature","properties":{"Shape__Area":335.328125},"geometry":{"type":"Polygon","coordinates":[[[-76.5134925519712,42.435729751882],[-76.5134747646607,42.4357680989805],[-76.5134415897226,42.4357600652398],[-76.5134258126737,42.435794001214],[-76.5133339055512,42.4357713450434],[-76.5133360154199,42.4357670240191],[-76.5133020391437,42.4357582695133],[-76.5133256575219,42.435705069347],[-76.513359735151,42.4357133737606],[-76.5133984256187,42.4356294782421],[-76.513489629477,42.435653755363],[-76.5134664152213,42.4357032637032],[-76.5134981860555,42.4357115675699],[-76.5135072295072,42.4356932043751],[-76.5135422080746,42.4357023195481],[-76.5135257252003,42.4357393170074],[-76.5134925519712,42.435729751882]]]}},{"type":"Feature","properties":{"Shape__Area":211.44140625},"geometry":{"type":"Polygon","coordinates":[[[-76.492905106307,42.4306838973992],[-76.4927274754054,42.430689690085],[-76.492725357434,42.4306540511335],[-76.4927512626367,42.4306532064225],[-76.4927488195563,42.430612085318],[-76.4929005450557,42.4306071373494],[-76.492905106307,42.4306838973992]]]}},{"type":"Feature","properties":{"Shape__Area":94.53515625},"geometry":{"type":"Polygon","coordinates":[[[-76.5022813947639,42.4331206241242],[-76.5022818165337,42.4331828428844],[-76.5021914916543,42.4331833191625],[-76.5021908695588,42.4331210102308],[-76.5022813947639,42.4331206241242]]]}},{"type":"Feature","properties":{"Shape__Area":231.63671875},"geometry":{"type":"Polygon","coordinates":[[[-76.5068127427235,42.4381541766599],[-76.5068192816695,42.4383001384632],[-76.50674007618,42.4383021562448],[-76.5067368496648,42.4382348931363],[-76.5067090772151,42.4382355948109],[-76.5067054636839,42.4381568058802],[-76.5068127427235,42.4381541766599]]]}},{"type":"Feature","properties":{"Shape__Area":231.0},"geometry":{"type":"Polygon","coordinates":[[[-76.4947247309657,42.4423683230631],[-76.4947251173704,42.4423787676628],[-76.4946860129818,42.442379547693],[-76.4946851394988,42.4423593777497],[-76.494725646964,42.4423583287164],[-76.4947227639819,42.4422701760822],[-76.4947760047635,42.4422694069612],[-76.4947758168827,42.4422604029839],[-76.4948580353729,42.4422589350726],[-76.4948609108167,42.4423525803863],[-76.4947845085883,42.442353961863],[-76.4947770702971,42.4423672822565],[-76.4947247309657,42.4423683230631]]]}},{"type":"Feature","properties":{"Shape__Area":219.6953125},"geometry":{"type":"Polygon","coordinates":[[[-76.4951943150519,42.4499869013655],[-76.4951945069266,42.4499931144278],[-76.4952588863508,42.4499921735901],[-76.4952590809273,42.4499955948491],[-76.4952905686562,42.4499950788629],[-76.4952912407966,42.4500168691455],[-76.495312599349,42.4500165254313],[-76.4953136474543,42.4500563245425],[-76.4952611013985,42.4500571844742],[-76.4952612971897,42.4500606066344],[-76.4951668352442,42.4500620645147],[-76.4951645284122,42.4500617026236],[-76.4951163953496,42.450062475854],[-76.4951140830897,42.4499949431746],[-76.4951299273733,42.4499945952506],[-76.495129636452,42.4499879319659],[-76.4951943150519,42.4499869013655]]]}},{"type":"Feature","properties":{"Shape__Area":160.47265625},"geometry":{"type":"Polygon","coordinates":[[[-76.5018523561849,42.4454819281819],[-76.5019023091313,42.4455449031398],[-76.5017876369683,42.4455947941678],[-76.5017411879842,42.4455360548308],[-76.501768476044,42.445524278616],[-76.5017650733982,42.4455197738311],[-76.5018523561849,42.4454819281819]]]}},{"type":"Feature","properties":{"Shape__Area":103.828125},"geometry":{"type":"Polygon","coordinates":[[[-76.4935497698235,42.4516229993331],[-76.4934089899866,42.4516209182415],[-76.4934101724357,42.4515770218457],[-76.4935509521743,42.4515791029358],[-76.4935497698235,42.4516229993331]]]}},{"type":"Feature","properties":{"Shape__Area":23.90234375},"geometry":{"type":"Polygon","coordinates":[[[-76.5069119060813,42.4405913944768],[-76.5068600712482,42.440592756264],[-76.5068587578376,42.4405653324486],[-76.5069105926482,42.440563970662],[-76.5069119060813,42.4405913944768]]]}},{"type":"Feature","properties":{"Shape__Area":243.8203125},"geometry":{"type":"Polygon","coordinates":[[[-76.4886789173937,42.4421091232796],[-76.4886870065237,42.4422634609168],[-76.4886003751556,42.4422660002066],[-76.4885911505999,42.4422659925829],[-76.4885848180394,42.442143981418],[-76.4885938412087,42.4421437187878],[-76.4885922863736,42.4421115725344],[-76.4886789173937,42.4421091232796]]]}},{"type":"Feature","properties":{"Shape__Area":41.60546875},"geometry":{"type":"Polygon","coordinates":[[[-76.5012759119437,42.4372593864179],[-76.5012758911506,42.4372753243074],[-76.501198293604,42.4372754487939],[-76.5011981141076,42.4372593307021],[-76.5011981348018,42.4372434837422],[-76.5012759326182,42.4372435394579],[-76.5012759119437,42.4372593864179]]]}},{"type":"Feature","properties":{"Shape__Area":378.23046875},"geometry":{"type":"Polygon","coordinates":[[[-76.4999847334096,42.4497315255511],[-76.4999856098849,42.4497507951384],[-76.5000094767099,42.449750092256],[-76.5000115151699,42.4498006973511],[-76.4999883509787,42.4498014007448],[-76.4999894215955,42.4498255320498],[-76.4997624913183,42.4498308595121],[-76.4997584040081,42.4497368534423],[-76.4999847334096,42.4497315255511]]]}},{"type":"Feature","properties":{"Shape__Area":282.97265625},"geometry":{"type":"Polygon","coordinates":[[[-76.4813002663968,42.4537079456378],[-76.4813227135076,42.4537178697215],[-76.4813257288868,42.453713911123],[-76.4813921672559,42.4537435033848],[-76.4813114784798,42.4538431973821],[-76.4812452402034,42.4538138744393],[-76.4812540825104,42.453803257976],[-76.4812413556374,42.4537975739059],[-76.4812649691902,42.453768241836],[-76.4812760924549,42.4537732033424],[-76.4812803136136,42.4537678953817],[-76.4811742909714,42.4537209794888],[-76.4811849427455,42.4537078429272],[-76.4811891622544,42.4537028041554],[-76.4812176936084,42.453671134819],[-76.4813002663968,42.4537079456378]]]}},{"type":"Feature","properties":{"Shape__Area":226.625},"geometry":{"type":"Polygon","coordinates":[[[-76.4948460240142,42.4430283298089],[-76.4948462163411,42.4430341827626],[-76.4948907354929,42.4430331367677],[-76.4948916023399,42.4430580782552],[-76.4949444432939,42.4430568586011],[-76.4949455045989,42.4430870237382],[-76.4949193343827,42.4430877237909],[-76.4948957717368,42.4430882457907],[-76.4948964485337,42.4431057137987],[-76.4947334122641,42.4431093700334],[-76.4947321100697,42.4430371556206],[-76.4947888603083,42.4430375595873],[-76.4947883697041,42.4430294556705],[-76.4948460240142,42.4430283298089]]]}},{"type":"Feature","properties":{"Shape__Area":344.06640625},"geometry":{"type":"Polygon","coordinates":[[[-76.5153385187076,42.4327554604066],[-76.5153829707507,42.4327554867641],[-76.5153830003916,42.4327280457985],[-76.515457087098,42.4327280896892],[-76.5154570574894,42.4327555306549],[-76.5156237526521,42.4327556292342],[-76.5156237053946,42.4327995347791],[-76.5155496186038,42.432799490996],[-76.5155495831219,42.4328324201544],[-76.5154310441949,42.4328323500017],[-76.5154310797389,42.4327994208433],[-76.5154199667204,42.4327994142602],[-76.5154199370956,42.4328268552255],[-76.5153458502729,42.4328268113107],[-76.51534587993,42.4327993703454],[-76.515338471251,42.4327993659513],[-76.5153385187076,42.4327554604066]]]}},{"type":"Feature","properties":{"Shape__Area":887.9296875},"geometry":{"type":"Polygon","coordinates":[[[-76.4914269246193,42.4478784168943],[-76.4915386104451,42.4479590933488],[-76.4914872137932,42.4479981302657],[-76.491496629642,42.4480050709605],[-76.4914470391558,42.4480425788092],[-76.4914935170579,42.4480762014589],[-76.4913642226331,42.4481742422566],[-76.491299614478,42.4481275498622],[-76.4913165789416,42.44811486756],[-76.491284225588,42.448091160297],[-76.4912656545157,42.4481051917424],[-76.4912050535941,42.4480615635064],[-76.4912238255095,42.4480473521735],[-76.4912118048013,42.4480386078782],[-76.4912514573994,42.4480087464334],[-76.4912426423897,42.4480024364059],[-76.4912833980234,42.4479714045558],[-76.4912415288646,42.4479410274755],[-76.4912848947442,42.4479081071077],[-76.4913334755539,42.4479433520378],[-76.4913592742023,42.447923833697],[-76.4913631806559,42.4479267186669],[-76.4914269246193,42.4478784168943]]]}},{"type":"Feature","properties":{"Shape__Area":88.48046875},"geometry":{"type":"Polygon","coordinates":[[[-76.496536643606,42.4464024811247],[-76.49654186995,42.4464664148544],[-76.4964596431355,42.4464699537378],[-76.496454617447,42.4464060210567],[-76.496536643606,42.4464024811247]]]}},{"type":"Feature","properties":{"Shape__Area":149.44140625},"geometry":{"type":"Polygon","coordinates":[[[-76.4883767856649,42.4394781332616],[-76.4884104260529,42.4395755863947],[-76.4883244792964,42.4395916322492],[-76.488291039311,42.4394943602163],[-76.4883767856649,42.4394781332616]]]}},{"type":"Feature","properties":{"Shape__Area":275.29296875},"geometry":{"type":"Polygon","coordinates":[[[-76.5068616830315,42.4265963862588],[-76.5069292670959,42.4266595501516],[-76.5068192272592,42.4267241269907],[-76.5067860859848,42.4266930401643],[-76.5067729451534,42.4267005956732],[-76.5067473126908,42.4266765370564],[-76.5067185243799,42.42669335605],[-76.506664857463,42.4266428964111],[-76.5067230375645,42.4266086295399],[-76.5067683949947,42.4266509791408],[-76.5068616830315,42.4265963862588]]]}},{"type":"Feature","properties":{"Shape__Area":1025.6953125},"geometry":{"type":"Polygon","coordinates":[[[-76.4916226080664,42.4397987119803],[-76.4915943071961,42.4398177782594],[-76.491594304043,42.4398199398617],[-76.4914761945425,42.439822276072],[-76.4914775435692,42.4398598247961],[-76.4913569274267,42.4398623389336],[-76.4913555787352,42.4398246101497],[-76.4913446501853,42.4398247814391],[-76.4913432974487,42.4397906547261],[-76.4913556289071,42.4397903045037],[-76.4913551472284,42.4397772480599],[-76.4913496323839,42.4397774236936],[-76.491348955158,42.4397609459773],[-76.4913295044441,42.439761470544],[-76.4913281523455,42.4397260834199],[-76.4913522146642,42.4397254725245],[-76.4913520246485,42.4397182682228],[-76.4913548326417,42.4397180904172],[-76.491354349119,42.4397062952831],[-76.491371293259,42.4397059487601],[-76.4913696511888,42.4396635373129],[-76.491284528092,42.4396653605085],[-76.4912828819656,42.4396249306028],[-76.4914475132819,42.4396216406681],[-76.4914473282539,42.4396110152548],[-76.4915186152347,42.4396094518456],[-76.4915190958946,42.4396232285231],[-76.4916299863696,42.4396208864936],[-76.4916315263206,42.4396650984428],[-76.4915929254392,42.4396659678507],[-76.4915946573477,42.4397152215977],[-76.4916168939776,42.4397296458889],[-76.4916177605657,42.4397546783448],[-76.4916147488683,42.4397570166984],[-76.4916193557582,42.4397601723122],[-76.4916341947202,42.439759824066],[-76.4916355513249,42.4397922393208],[-76.4916223170368,42.4397924087922],[-76.4916226080664,42.4397987119803]]]}},{"type":"Feature","properties":{"Shape__Area":186.44140625},"geometry":{"type":"Polygon","coordinates":[[[-76.4964349798465,42.4492246584467],[-76.4964358621965,42.4492388855312],[-76.4964625371455,42.4492380954867],[-76.4964661689359,42.4492948247429],[-76.4963052193484,42.4493005554599],[-76.4963007058818,42.4492292389969],[-76.4964349798465,42.4492246584467]]]}},{"type":"Feature","properties":{"Shape__Area":201.7265625},"geometry":{"type":"Polygon","coordinates":[[[-76.5029280877555,42.4320337377787],[-76.5028586144976,42.4320352195304],[-76.5028575415112,42.4320113582637],[-76.5028389952838,42.4320117053605],[-76.5028385090896,42.4320003595038],[-76.5028552505654,42.4320000111401],[-76.5028547685147,42.4319854242251],[-76.502822086235,42.4319863015711],[-76.5028191736888,42.431912735535],[-76.5028453379549,42.4319120336725],[-76.5028446621771,42.4318921339814],[-76.5028944867936,42.4318909985703],[-76.5028951674232,42.431907116126],[-76.5029194282697,42.43190659297],[-76.5029226409509,42.431981239568],[-76.5029154229451,42.4319814145637],[-76.502916101163,42.4319994236367],[-76.5029267281398,42.4319992510321],[-76.5029280877555,42.4320337377787]]]}},{"type":"Feature","properties":{"Shape__Area":227.16015625},"geometry":{"type":"Polygon","coordinates":[[[-76.5148246469273,42.4397513575577],[-76.5148028889565,42.4397523348754],[-76.5148076354999,42.4398135657813],[-76.5148000151298,42.4398141014019],[-76.5148004929439,42.4398351712588],[-76.5147159698658,42.4398389028296],[-76.5147140893907,42.4398167517804],[-76.5147096768414,42.4398171092564],[-76.5147023620607,42.4397213910619],[-76.5147109851827,42.4397208551478],[-76.5147095990011,42.4397046472321],[-76.5147895102179,42.4397010929671],[-76.5147903984545,42.4397136093802],[-76.5148187738126,42.4397122759027],[-76.5148239842081,42.4397148007392],[-76.5148246469273,42.4397513575577]]]}},{"type":"Feature","properties":{"Shape__Area":40.96875},"geometry":{"type":"Polygon","coordinates":[[[-76.5053546293148,42.4387415549726],[-76.5052657438289,42.438743209534],[-76.5052648140285,42.4387157769499],[-76.5053536982602,42.4387141223883],[-76.5053546293148,42.4387415549726]]]}},{"type":"Feature","properties":{"Shape__Area":273.7109375},"geometry":{"type":"Polygon","coordinates":[[[-76.4981891012887,42.452543814874],[-76.4981961706987,42.4526551114668],[-76.4981871447219,42.4526554648792],[-76.4981880282433,42.4526700520592],[-76.4980719982594,42.4526740180265],[-76.498070619141,42.452655558323],[-76.498066908526,42.4526557356212],[-76.4980617019834,42.4525765853167],[-76.4980510712717,42.4525769375247],[-76.4980496949707,42.4525564071522],[-76.4980613286446,42.4525560556904],[-76.4980608370578,42.4525483119169],[-76.4981891012887,42.452543814874]]]}},{"type":"Feature","properties":{"Shape__Area":13.65625},"geometry":{"type":"Polygon","coordinates":[[[-76.5138629464916,42.4337553270122],[-76.5138924959692,42.4337570014577],[-76.5138896704505,42.4337843623986],[-76.5138601221745,42.4337826888535],[-76.5138629464916,42.4337553270122]]]}},{"type":"Feature","properties":{"Shape__Area":206.1015625},"geometry":{"type":"Polygon","coordinates":[[[-76.5172527041264,42.44941575134],[-76.5172680838696,42.4494754576734],[-76.5172546440116,42.4494773405401],[-76.5172639336285,42.4495124617876],[-76.5171466935001,42.4495294123753],[-76.5171227232741,42.4494380963108],[-76.5171507044605,42.449434240313],[-76.5171496058409,42.4494304584519],[-76.5172527041264,42.44941575134]]]}},{"type":"Feature","properties":{"Shape__Area":440.33203125},"geometry":{"type":"Polygon","coordinates":[[[-76.4763613350457,42.4335221121253],[-76.4763624018424,42.4335430026544],[-76.4763747333714,42.433542654043],[-76.4763754040066,42.4335606632599],[-76.4763609674915,42.433561009907],[-76.4763619344771,42.4335819903723],[-76.4762581722525,42.4335844143239],[-76.4762538644571,42.4335233622297],[-76.4762517267812,42.4334247644782],[-76.4764501254003,42.4334223387792],[-76.4764525634248,42.4335210268366],[-76.4763613350457,42.4335221121253]]]}},{"type":"Feature","properties":{"Shape__Area":35.4375},"geometry":{"type":"Polygon","coordinates":[[[-76.5210428586448,42.4397386079831],[-76.5210907779576,42.4397428653918],[-76.521083516331,42.4397862619036],[-76.5210355969865,42.4397820035916],[-76.5210428586448,42.4397386079831]]]}},{"type":"Feature","properties":{"Shape__Area":214.75},"geometry":{"type":"Polygon","coordinates":[[[-76.4974403608084,42.4524343065907],[-76.4974462302434,42.452542000266],[-76.4973277949185,42.4525456935903],[-76.4973219247167,42.4524378198502],[-76.4974403608084,42.4524343065907]]]}},{"type":"Feature","properties":{"Shape__Area":1350.09765625},"geometry":{"type":"Polygon","coordinates":[[[-76.510679170562,42.4390985104166],[-76.5106623764513,42.4389029766801],[-76.510740018962,42.4388993184648],[-76.5107284933675,42.4387651286832],[-76.510835712867,42.4387600765713],[-76.5108472386903,42.4388942654421],[-76.5108731195204,42.4388930466013],[-76.5108726769506,42.4388878975416],[-76.5109946858634,42.4388821487983],[-76.51101192237,42.4390828306456],[-76.510679170562,42.4390985104166]]]}},{"type":"Feature","properties":{"Shape__Area":158.56640625},"geometry":{"type":"Polygon","coordinates":[[[-76.4949745272327,42.43184978153],[-76.4951086890523,42.4318987771716],[-76.4950711170769,42.4319553839958],[-76.494936954913,42.4319065683694],[-76.4949745272327,42.43184978153]]]}},{"type":"Feature","properties":{"Shape__Area":46.26171875},"geometry":{"type":"Polygon","coordinates":[[[-76.5145932846731,42.4490729307952],[-76.5145803200032,42.4490993952103],[-76.51449281411,42.4490691784043],[-76.514511694944,42.4490419973173],[-76.5145932846731,42.4490729307952]]]}},{"type":"Feature","properties":{"Shape__Area":479.625},"geometry":{"type":"Polygon","coordinates":[[[-76.4934245693376,42.4468859451085],[-76.4934249525337,42.4468976510115],[-76.4934395925954,42.4468973023763],[-76.4934413438321,42.4469350314049],[-76.4934272058073,42.4469353804341],[-76.4934299280566,42.446994539885],[-76.4934442678858,42.4469941910137],[-76.4934463174539,42.4470332698139],[-76.4934328808133,42.4470336193938],[-76.4934340407756,42.4470572358741],[-76.4932734854023,42.4470614330496],[-76.4932710751327,42.447011162428],[-76.4932668640069,42.4470113391778],[-76.493265984413,42.4469948613346],[-76.4932712993099,42.446994685452],[-76.493268862246,42.4469461505725],[-76.4932628459478,42.4469463259038],[-76.4932618708031,42.4469269670495],[-76.4932681873544,42.4469267919541],[-76.4932663349946,42.4468901431943],[-76.4934245693376,42.4468859451085]]]}},{"type":"Feature","properties":{"Shape__Area":15.3125},"geometry":{"type":"Polygon","coordinates":[[[-76.5135141120794,42.4331957269346],[-76.5135292280829,42.433214735058],[-76.5134935223576,42.433230200179],[-76.5134785061192,42.4332111020823],[-76.5135141120794,42.4331957269346]]]}},{"type":"Feature","properties":{"Shape__Area":237.8125},"geometry":{"type":"Polygon","coordinates":[[[-76.4810180734764,42.4392891075593],[-76.4811175593711,42.4393329559837],[-76.4810426140478,42.4394263522121],[-76.4810375042623,42.4394240969183],[-76.4810275585913,42.4394365138948],[-76.4809331823749,42.4393949206916],[-76.4810180734764,42.4392891075593]]]}},{"type":"Feature","properties":{"Shape__Area":348.703125},"geometry":{"type":"Polygon","coordinates":[[[-76.4883830576446,42.4397734743654],[-76.4883837398022,42.4397861708702],[-76.4883941577932,42.4397924824541],[-76.4883948379147,42.4398065293974],[-76.488384797781,42.4398156149469],[-76.4883808876164,42.4398156117081],[-76.4883810821867,42.4398195740612],[-76.4883982282766,42.4398191381156],[-76.4884011525755,42.4398739756169],[-76.4883802976628,42.439874678579],[-76.4883807855222,42.4398832326721],[-76.4883238357103,42.4398848969432],[-76.4883240279664,42.4398903888931],[-76.4882900382615,42.4398913519401],[-76.4882895490551,42.4398836972388],[-76.4882476390747,42.4398848337648],[-76.4882471519249,42.4398750183605],[-76.4882006300477,42.4398764202314],[-76.4881959592595,42.4397822330455],[-76.4882435845765,42.4397809221211],[-76.4882433891969,42.4397775008434],[-76.4883830576446,42.4397734743654]]]}},{"type":"Feature","properties":{"Shape__Area":263.453125},"geometry":{"type":"Polygon","coordinates":[[[-76.4821451221005,42.4376440603867],[-76.4822805680765,42.4376441798666],[-76.4822804744777,42.4377024360003],[-76.4822864908472,42.4377024413038],[-76.4822864418071,42.4377329657242],[-76.4822795235865,42.4377329596257],[-76.4822795069488,42.4377433147911],[-76.4822331871529,42.4377439041546],[-76.482207220728,42.4377438812507],[-76.482207168611,42.437776295385],[-76.4821449094445,42.4377762404448],[-76.4821451221005,42.4376440603867]]]}},{"type":"Feature","properties":{"Shape__Area":2796.37109375},"geometry":{"type":"Polygon","coordinates":[[[-76.479558481699,42.4489025009929],[-76.4795580733414,42.449028378563],[-76.4793278449801,42.4490278097863],[-76.4793285705957,42.4488919383623],[-76.4793278066679,42.448808468887],[-76.4793067490061,42.4488086298374],[-76.4793061503546,42.4487457808016],[-76.4793295149851,42.4487458020028],[-76.4793252975799,42.4483249449034],[-76.479317776307,42.4483250281081],[-76.4793184795389,42.4483246686293],[-76.4793339217301,42.4483246826405],[-76.4795201402573,42.4483179182889],[-76.4795201387707,42.4483188185798],[-76.4795587439347,42.4483186734773],[-76.4795610542772,42.4487417795727],[-76.4795787017446,42.4487417955474],[-76.4795790902787,42.4488105881202],[-76.4795582321307,42.4488106592682],[-76.479558481699,42.4489025009929]]]}},{"type":"Feature","properties":{"Shape__Area":581.98046875},"geometry":{"type":"Polygon","coordinates":[[[-76.5105126807259,42.4380216146797],[-76.5107431113647,42.4381597938845],[-76.5106439529298,42.4382506733345],[-76.5104132210622,42.4381122227525],[-76.5105126807259,42.4380216146797]]]}},{"type":"Feature","properties":{"Shape__Area":213.33984375},"geometry":{"type":"Polygon","coordinates":[[[-76.5130995179448,42.4345775680406],[-76.513148453015,42.4346358119492],[-76.5131232916169,42.4346474112398],[-76.5131389515351,42.4346660488217],[-76.5130760492017,42.4346950483837],[-76.5130603905103,42.434676410794],[-76.5129534580106,42.434725709236],[-76.5129240960511,42.4346907633837],[-76.5130687706315,42.4346240647107],[-76.5130491963822,42.4346007674961],[-76.5130995179448,42.4345775680406]]]}},{"type":"Feature","properties":{"Shape__Area":325.55859375},"geometry":{"type":"Polygon","coordinates":[[[-76.4866712742378,42.4403282297401],[-76.4866721473074,42.4403477695454],[-76.4867016250068,42.4403470741633],[-76.4867049048263,42.4404306349646],[-76.4866977866924,42.4404308090229],[-76.4867000044664,42.4404880776585],[-76.4866071591059,42.4404900700285],[-76.4866068684375,42.4404838568582],[-76.4865948370417,42.4404842068229],[-76.4865934835764,42.4404514304978],[-76.4866053139961,42.4404513504517],[-76.4866044385597,42.4404341523075],[-76.4865699481742,42.4404348434334],[-76.4865685941688,42.4404024272246],[-76.4865532530349,42.4404027743911],[-76.4865523846807,42.4403809838553],[-76.486567925148,42.4403806368573],[-76.4865667687899,42.4403501114313],[-76.4865700788176,42.4403499341667],[-76.4865694063338,42.4403303945299],[-76.4866712742378,42.4403282297401]]]}},{"type":"Feature","properties":{"Shape__Area":263.59765625},"geometry":{"type":"Polygon","coordinates":[[[-76.5211837341797,42.4431637974202],[-76.5211858407845,42.4431034474147],[-76.5211265809584,42.443102312413],[-76.5211277305977,42.4430693938195],[-76.5211869903929,42.4430705288206],[-76.5211887129794,42.4430211513783],[-76.5212850093164,42.4430229951251],[-76.5212800307351,42.4431656411714],[-76.5211837341797,42.4431637974202]]]}},{"type":"Feature","properties":{"Shape__Area":178.80078125},"geometry":{"type":"Polygon","coordinates":[[[-76.5044793524047,42.4365610223193],[-76.5044811954281,42.4366100959175],[-76.5044209413325,42.4366112248377],[-76.5044205602426,42.4365952875693],[-76.5044078271814,42.4365956389209],[-76.5044046099119,42.4365230630979],[-76.5044081187894,42.4365230655138],[-76.5044069512133,42.4364956021384],[-76.5044032417954,42.4364955995844],[-76.5044016864994,42.4364565203471],[-76.5044579309395,42.4364552986488],[-76.5044592860936,42.4364931173358],[-76.5044803398002,42.4364927717022],[-76.5044830618265,42.4365610248708],[-76.5044793524047,42.4365610223193]]]}},{"type":"Feature","properties":{"Shape__Area":12.05859375},"geometry":{"type":"Polygon","coordinates":[[[-76.5152045940483,42.433306144265],[-76.5152502055743,42.4333076127348],[-76.5152492867348,42.4333233691457],[-76.5152036740786,42.4333218115457],[-76.5152045940483,42.433306144265]]]}},{"type":"Feature","properties":{"Shape__Area":255.30078125},"geometry":{"type":"Polygon","coordinates":[[[-76.5063093986339,42.4327939393838],[-76.5063100643852,42.4328239232457],[-76.5063239986386,42.4328235724894],[-76.5063243790861,42.4328404109566],[-76.5063915472238,42.4328392856754],[-76.5063930799933,42.4328977230318],[-76.5062652605533,42.4328997087498],[-76.506265743181,42.4329144757076],[-76.5062213327988,42.4329150760489],[-76.5062179708167,42.4327954084178],[-76.5063093986339,42.4327939393838]]]}},{"type":"Feature","properties":{"Shape__Area":10.91796875},"geometry":{"type":"Polygon","coordinates":[[[-76.5201983960754,42.418695111203],[-76.520195912413,42.4187169870509],[-76.5201663882209,42.4187151476857],[-76.5201688718935,42.4186932718384],[-76.5201983960754,42.418695111203]]]}},{"type":"Feature","properties":{"Shape__Area":388.21484375},"geometry":{"type":"Polygon","coordinates":[[[-76.4967412330852,42.4457920655272],[-76.4967173673551,42.4457930378184],[-76.4967164928029,42.4457731379895],[-76.4967030698466,42.4457637639024],[-76.4967023878317,42.4457488167287],[-76.4967140320378,42.4457399108304],[-76.4967133544854,42.4457217226064],[-76.4967346123894,42.4457210184338],[-76.4967425343483,42.4457210244182],[-76.4967420383886,42.4457166117093],[-76.4969096965498,42.4457114256075],[-76.4969095009282,42.4457078242895],[-76.4969951341056,42.4457050970115],[-76.4969997336779,42.445787578085],[-76.4969137991707,42.4457901250806],[-76.4969072700654,42.4457982236922],[-76.4968836063703,42.4457990161118],[-76.4968769969181,42.4457921680028],[-76.4967857487056,42.4457949809823],[-76.4967859414702,42.4458006529715],[-76.4967709002893,42.4458011826901],[-76.4967704072778,42.4457955095739],[-76.4967414277042,42.4457963879791],[-76.4967412330852,42.4457920655272]]]}},{"type":"Feature","properties":{"Shape__Area":105.328125},"geometry":{"type":"Polygon","coordinates":[[[-76.4958774380224,42.45298300668],[-76.4958792514554,42.4530497286659],[-76.4958333216896,42.4530503238192],[-76.495833988446,42.4530752660034],[-76.4958022999247,42.453075691956],[-76.4958016302101,42.4530520101774],[-76.4957972183425,42.4530520068083],[-76.4957952069969,42.4529842943494],[-76.4958774380224,42.45298300668]]]}}]} \ No newline at end of file diff --git a/inst/examples/buildings_app.R b/inst/examples/buildings_app.R index 85034c05..94586338 100644 --- a/inst/examples/buildings_app.R +++ b/inst/examples/buildings_app.R @@ -1,32 +1,104 @@ library(shiny) library(leaflet) +library(yyjsonr) +library(sf) library(leaflet.extras2) +options("shiny.autoreload" = TRUE) +cols <- c("green","orange","red","pink","yellow","blue","lightblue") +darkcols <- c("lightgray","gray","#c49071","#876302","#443408") + +## Custom GeoJSON ########### +## Get a Sample Building Dataset from +# https://hub.arcgis.com/datasets/IthacaNY::buildings/explore?location=42.432557%2C-76.486649%2C13.42 +geojson <- yyjsonr::read_geojson_file("Buildings_mini.geojson") +geojson$height= sample(seq(50,100,5), nrow(geojson), replace = TRUE) +geojson$color= sample(cols, nrow(geojson), replace = TRUE) +geojson$wallColor= sample(cols, nrow(geojson), replace = TRUE) +geojson$roofColor= sample(darkcols, nrow(geojson), replace = TRUE) +geojson$shape= sample(c("cylinder","sphere",""), nrow(geojson), replace = TRUE) +geojson$roofHeight= geojson$height + sample(seq(1,10,1), nrow(geojson), replace = TRUE) +geojson$roofShape= sample(c("dome","pyramidal", "butterfly","gabled","half-hipped", + "gambrel","onion"), nrow(geojson), replace = TRUE) +geojson <- yyjsonr::write_geojson_str(geojson) +class(geojson) <- "json" + +## UI ########### ui <- fluidPage( - leafletOutput("map", height = "700px"), - dateInput("date", "Date"), - sliderInput("time", "Time", 0, max = 24, value = 4, step = 1) - # actionButton("update", "Update Date") + titlePanel("OSM Buildings (2.5D)"), + sidebarLayout( + sidebarPanel( + h4("Use the OSM Buildings or a Custom GeoJSON") + , selectInput("src", label = "Data Source", choices = c("OSM", "GeoJSON")) + , h4("Change the Date and Time-Slider to Adapt the Shadow") + , dateInput("date", "Date") + , sliderInput("time", "Time", 7, max =20, value = 11, step = 1) + , h4("Change the Style and the Data") + , actionButton("style", "Update Style") + , actionButton("data", "Update Data") + ), + mainPanel( + leafletOutput("map", height = "700px") + ), + fluid = TRUE ) +) +## SERVER ########### server <- function(input, output, session) { output$map <- renderLeaflet({ - leaflet() %>% - # addTiles() %>% - # addProviderTiles("CartoDB.DarkMatter") %>% - addBuildings() %>% - addMarkers(data = breweries91) %>% - setView(lng = 13.40438, lat = 52.51836, zoom = 16) + m <- leaflet() %>% + addProviderTiles("CartoDB") + + if (input$src == "OSM") { + m <- m %>% + addBuildings( + group = "Buildings" + # , eachFn = leaflet::JS("function(e) { console.log('each feature:', e); }") + # , clickFn = leaflet::JS("function(e) { console.log('clicked:', e); }") + ) + } else { + m <- m %>% + addBuildings( + group = "Buildings" + , buildingURL = NULL + , data = geojson + ) + } + + m %>% + addLayersControl(overlayGroups = "Buildings") %>% + setView(lng = -76.51, lat = 42.433, zoom = 15) }) observe({ - # observeEvent(input$update, { - # browser() - date <- input$date time <- formatC(input$time, width = 2, format = "d", flag = "0") - updatetime <- paste0(date, " ", time, ":00:00") + updatetime <- paste0(input$date, " ", time, ":00:00") leafletProxy("map") %>% updateBuildingTime(time = as.POSIXct(updatetime)) }) + observeEvent(input$style, { + leafletProxy("map") %>% + setBuildingStyle(style = list(color = sample(cols, 1), + wallColor = sample(cols, 1), + roofColor = sample(cols, 1), + roofShape = sample(c("dome","pyramidal", "butterfly","gabled","half-hipped", + "gambrel","onion"), 1), + shadows = sample(c(TRUE, FALSE), 1))) + }) + observeEvent(input$data, { + geojson <- yyjsonr::read_geojson_file("Buildings_mini.geojson") + filtered <- geojson[sample(1:nrow(geojson), 10, F),] + filtered$height= sample(seq(50,140,5), nrow(filtered), replace = TRUE) + filtered$color= sample(cols, nrow(filtered), replace = TRUE) + filtered$wallColor= sample(cols, nrow(filtered), replace = TRUE) + filtered$roofColor= sample(cols, nrow(filtered), replace = TRUE) + filtered <- yyjsonr::write_geojson_str(filtered) + class(filtered) <- "json" + + leafletProxy("map") %>% + setBuildingData(data = filtered) + }) } + shinyApp(ui, server) diff --git a/inst/htmlwidgets/lfx-building/OSMBuildings.js b/inst/htmlwidgets/lfx-building/OSMBuildings.js deleted file mode 100644 index 2c141700..00000000 --- a/inst/htmlwidgets/lfx-building/OSMBuildings.js +++ /dev/null @@ -1,2466 +0,0 @@ -const OSMBuildings = (function() { - -const - m = Math, - exp = m.exp, - log = m.log, - sin = m.sin, - cos = m.cos, - tan = m.tan, - atan = m.atan, - atan2 = m.atan2, - min = m.min, - max = m.max, - sqrt = m.sqrt, - ceil = m.ceil, - pow = m.pow; - - -/** - * @class - */ -class Qolor { - - /** - * @constructor - * @param r {Number} 0.0 .. 1.0 red value of a color - * @param g {Number} 0.0 .. 1.0 green value of a color - * @param b {Number} 0.0 .. 1.0 blue value of a color - * @param a {Number} 0.0 .. 1.0 alpha value of a color, default 1 - */ - constructor (r, g, b, a = 1) { - this.r = this._clamp(r, 1); - this.g = this._clamp(g, 1); - this.b = this._clamp(b, 1); - this.a = this._clamp(a, 1); - } - - /** - * @param str {String} can be any color dfinition like: 'red', '#0099ff', 'rgb(64, 128, 255)', 'rgba(64, 128, 255, 0.5)' - */ - static parse (str) { - if (typeof str === 'string') { - str = str.toLowerCase(); - str = Qolor.w3cColors[str] || str; - - let m; - - if ((m = str.match(/^#?(\w{2})(\w{2})(\w{2})$/))) { - return new Qolor(parseInt(m[1], 16)/255, parseInt(m[2], 16)/255, parseInt(m[3], 16)/255); - } - - if ((m = str.match(/^#?(\w)(\w)(\w)$/))) { - return new Qolor(parseInt(m[1]+m[1], 16)/255, parseInt(m[2]+m[2], 16)/255, parseInt(m[3]+m[3], 16)/255); - } - - if ((m = str.match(/rgba?\((\d+)\D+(\d+)\D+(\d+)(\D+([\d.]+))?\)/))) { - return new Qolor( - parseFloat(m[1])/255, - parseFloat(m[2])/255, - parseFloat(m[3])/255, - m[4] ? parseFloat(m[5]) : 1 - ); - } - } - - return new Qolor(); - } - - static fromHSL (h, s, l, a) { - const qolor = new Qolor().fromHSL(h, s, l); - qolor.a = a === undefined ? 1 : a; - return qolor; - } - - //*************************************************************************** - - _hue2rgb(p, q, t) { - if (t<0) t += 1; - if (t>1) t -= 1; - if (t<1/6) return p + (q - p)*6*t; - if (t<1/2) return q; - if (t<2/3) return p + (q - p)*(2/3 - t)*6; - return p; - } - - _clamp(v, max) { - if (v === undefined) { - return; - } - return Math.min(max, Math.max(0, v || 0)); - } - - //*************************************************************************** - - isValid () { - return this.r !== undefined && this.g !== undefined && this.b !== undefined; - } - - toHSL () { - if (!this.isValid()) { - return; - } - - const max = Math.max(this.r, this.g, this.b); - const min = Math.min(this.r, this.g, this.b); - const range = max - min; - const l = (max + min)/2; - - // achromatic - if (!range) { - return { h: 0, s: 0, l: l }; - } - - const s = l > 0.5 ? range/(2 - max - min) : range/(max + min); - - let h; - switch (max) { - case this.r: - h = (this.g - this.b)/range + (this.g 0 ? WINDING_CLOCKWISE : WINDING_COUNTER_CLOCKWISE; -} - -// enforce a polygon winding direcetion. Needed for proper backface culling. -function makeWinding (points, direction) { - let winding = getWinding(points); - if (winding === direction) { - return points; - } - let revPoints = []; - for (let i = points.length-2; i >= 0; i -= 2) { - revPoints.push(points[i], points[i+1]); - } - return revPoints; -} - -function alignProperties(prop) { - const item = {}; - - prop = prop || {}; - - item.height = prop.height || (prop.levels ? prop.levels *METERS_PER_LEVEL : DEFAULT_HEIGHT); - item.minHeight = prop.minHeight || (prop.minLevel ? prop.minLevel*METERS_PER_LEVEL : 0); - - const wallColor = prop.material ? getMaterialColor(prop.material) : (prop.wallColor || prop.color); - if (wallColor) { - item.wallColor = wallColor; - } - - const roofColor = prop.roofMaterial ? getMaterialColor(prop.roofMaterial) : prop.roofColor; - if (roofColor) { - item.roofColor = roofColor; - } - - switch (prop.shape) { - case 'cylinder': - case 'cone': - case 'dome': - case 'sphere': - item.shape = prop.shape; - item.isRotational = true; - break; - - case 'pyramid': - item.shape = prop.shape; - break; - } - - switch (prop.roofShape) { - case 'cone': - case 'dome': - item.roofShape = prop.roofShape; - item.isRotational = true; - break; - - case 'pyramid': - item.roofShape = prop.roofShape; - break; - } - - if (item.roofShape && prop.roofHeight) { - item.roofHeight = prop.roofHeight; - item.height = max(0, item.height-item.roofHeight); - } else { - item.roofHeight = 0; - } - - return item; -} - -function getGeometries (geometry) { - let - polygon, - geometries = [], sub; - - switch (geometry.type) { - case 'GeometryCollection': - geometries = []; - for (let i = 0, il = geometry.geometries.length; i < il; i++) { - if ((sub = getGeometries(geometry.geometries[i]))) { - geometries.push.apply(geometries, sub); - } - } - return geometries; - - case 'MultiPolygon': - geometries = []; - for (let i = 0, il = geometry.coordinates.length; i < il; i++) { - if ((sub = getGeometries({ type: 'Polygon', coordinates: geometry.coordinates[i] }))) { - geometries.push.apply(geometries, sub); - } - } - return geometries; - - case 'Polygon': - polygon = geometry.coordinates; - break; - - default: return []; - } - - let - p, lat = 1, lon = 0, - outer = [], inner = []; - - p = polygon[0]; - for (let i = 0, il = p.length; i < il; i++) { - outer.push(p[i][lat], p[i][lon]); - } - outer = makeWinding(outer, WINDING_CLOCKWISE); - - for (let i = 0, il = polygon.length-1; i < il; i++) { - p = polygon[i+1]; - inner[i] = []; - for (let j = 0, jl = p.length; j < jl; j++) { - inner[i].push(p[j][lat], p[j][lon]); - } - inner[i] = makeWinding(inner[i], WINDING_COUNTER_CLOCKWISE); - } - - return [{ - outer: outer, - inner: inner.length ? inner : null - }]; -} - -function clone (obj) { - let res = {}; - for (const p in obj) { - if (obj.hasOwnProperty(p)) { - res[p] = obj[p]; - } - } - return res; -} - -class GeoJSON { - - static read (geojson) { - if (!geojson || geojson.type !== 'FeatureCollection') { - return []; - } - - const collection = geojson.features; - const res = []; - - for (let i = 0, il = collection.length; i < il; i++) { - const feature = collection[i]; - - if (feature.type !== 'Feature' || onEach(feature) === false) { - continue; - } - - const baseItem = alignProperties(feature.properties); - const geometries = getGeometries(feature.geometry); - - for (let j = 0, jl = geometries.length; j < jl; j++) { - const item = clone(baseItem); - item.footprint = geometries[j].outer; - if (item.isRotational) { - item.radius = getLonDelta(item.footprint); - } - - if (geometries[j].inner) { - item.holes = geometries[j].inner; - } - if (feature.id || feature.properties.id) { - item.id = feature.id || feature.properties.id; - } - - if (feature.properties.relationId) { - item.relationId = feature.properties.relationId; - } - - res.push(item); // TODO: clone base properties! - } - } - - return res; - } -} - -let - VERSION = '0.3.2', - ATTRIBUTION = '© OSM Buildings', - - DATA_SRC = 'https://{s}.data.osmbuildings.org/0.2/{k}/tile/{z}/{x}/{y}.json', - - PI = Math.PI, - HALF_PI = PI/2, - QUARTER_PI = PI/4, - - MAP_TILE_SIZE = 256, // map tile size in pixels - ZOOM, MAP_SIZE, - - MIN_ZOOM = 15, - - LAT = 'latitude', LON = 'longitude', - - WIDTH = 0, HEIGHT = 0, - CENTER_X = 0, CENTER_Y = 0, - ORIGIN_X = 0, ORIGIN_Y = 0, - - WALL_COLOR = Qolor.parse('rgba(200, 190, 180)'), - ALT_COLOR = WALL_COLOR.lightness(0.8), - ROOF_COLOR = WALL_COLOR.lightness(1.2), - - WALL_COLOR_STR = ''+ WALL_COLOR, - ALT_COLOR_STR = ''+ ALT_COLOR, - ROOF_COLOR_STR = ''+ ROOF_COLOR, - - PIXEL_PER_DEG = 0, - - MAX_HEIGHT, // taller buildings will be cut to this - DEFAULT_HEIGHT = 5, - - CAM_X, CAM_Y, CAM_Z = 450, - - IS_ZOOMING; - -function onEach () {} - -function onClick () {} - - -function getDistance (p1, p2) { - const - dx = p1.x-p2.x, - dy = p1.y-p2.y; - return dx*dx + dy*dy; -} - -function isRotational (polygon) { - const length = polygon.length; - if (length < 16) { - return false; - } - - let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; - for (let i = 0; i < length-1; i+=2) { - minX = Math.min(minX, polygon[i]); - maxX = Math.max(maxX, polygon[i]); - minY = Math.min(minY, polygon[i+1]); - maxY = Math.max(maxY, polygon[i+1]); - } - - const - width = maxX-minX, - height = (maxY-minY), - ratio = width/height; - - if (ratio < 0.85 || ratio > 1.15) { - return false; - } - - const - center = { x:minX+width/2, y:minY+height/2 }, - radius = (width+height)/4, - sqRadius = radius*radius; - - for (let i = 0; i < length-1; i+=2) { - const dist = getDistance({ x:polygon[i], y:polygon[i+1] }, center); - if (dist/sqRadius < 0.8 || dist/sqRadius > 1.2) { - return false; - } - } - - return true; -} - -function getSquareSegmentDistance (px, py, p1x, p1y, p2x, p2y) { - let - dx = p2x-p1x, - dy = p2y-p1y, - t; - if (dx !== 0 || dy !== 0) { - t = ((px-p1x) * dx + (py-p1y) * dy) / (dx*dx + dy*dy); - if (t > 1) { - p1x = p2x; - p1y = p2y; - } else if (t > 0) { - p1x += dx*t; - p1y += dy*t; - } - } - dx = px-p1x; - dy = py-p1y; - return dx*dx + dy*dy; -} - -function simplifyPolygon (buffer) { - let - sqTolerance = 2, - len = buffer.length/2, - markers = new Uint8Array(len), - - first = 0, last = len-1, - - maxSqDist, - sqDist, - index, - firstStack = [], lastStack = [], - newBuffer = []; - - markers[first] = markers[last] = 1; - - while (last) { - maxSqDist = 0; - for (let i = first+1; i < last; i++) { - sqDist = getSquareSegmentDistance( - buffer[i *2], buffer[i *2 + 1], - buffer[first*2], buffer[first*2 + 1], - buffer[last *2], buffer[last *2 + 1] - ); - if (sqDist > maxSqDist) { - index = i; - maxSqDist = sqDist; - } - } - - if (maxSqDist > sqTolerance) { - markers[index] = 1; - - firstStack.push(first); - lastStack.push(index); - - firstStack.push(index); - lastStack.push(last); - } - - first = firstStack.pop(); - last = lastStack.pop(); - } - - for (let i = 0; i < len; i++) { - if (markers[i]) { - newBuffer.push(buffer[i*2], buffer[i*2 + 1]); - } - } - - return newBuffer; -} - -function getCenter (footprint) { - let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; - for (let i = 0, il = footprint.length-3; i < il; i += 2) { - minX = min(minX, footprint[i]); - maxX = max(maxX, footprint[i]); - minY = min(minY, footprint[i+1]); - maxY = max(maxY, footprint[i+1]); - } - return { x:minX+(maxX-minX)/2 <<0, y:minY+(maxY-minY)/2 <<0 }; -} - -let EARTH_RADIUS = 6378137; - -function getLonDelta (footprint) { - let minLon = 180, maxLon = -180; - for (let i = 0, il = footprint.length; i < il; i += 2) { - minLon = min(minLon, footprint[i+1]); - maxLon = max(maxLon, footprint[i+1]); - } - return (maxLon-minLon)/2; -} - - -function rad (deg) { - return deg * PI / 180; -} - -function deg (rad) { - return rad / PI * 180; -} - -function pixelToGeo (x, y) { - const res = {}; - x /= MAP_SIZE; - y /= MAP_SIZE; - res[LAT] = y <= 0 ? 90 : y >= 1 ? -90 : deg(2 * atan(exp(PI * (1 - 2*y))) - HALF_PI); - res[LON] = (x === 1 ? 1 : (x%1 + 1) % 1) * 360 - 180; - return res; -} - -function geoToPixel (lat, lon) { - const - latitude = min(1, max(0, 0.5 - (log(tan(QUARTER_PI + HALF_PI * lat / 180)) / PI) / 2)), - longitude = lon/360 + 0.5; - return { - x: longitude*MAP_SIZE <<0, - y: latitude *MAP_SIZE <<0 - }; -} - -function fromRange (sVal, sMin, sMax, dMin, dMax) { - sVal = min(max(sVal, sMin), sMax); - const rel = (sVal-sMin) / (sMax-sMin), - range = dMax-dMin; - return min(max(dMin + rel*range, dMin), dMax); -} - -function isVisible (polygon) { - const - maxX = WIDTH+ORIGIN_X, - maxY = HEIGHT+ORIGIN_Y; - - // TODO: checking footprint is sufficient for visibility - NOT VALID FOR SHADOWS! - for (let i = 0, il = polygon.length-3; i < il; i+=2) { - if (polygon[i] > ORIGIN_X && polygon[i] < maxX && polygon[i+1] > ORIGIN_Y && polygon[i+1] < maxY) { - return true; - } - } - return false; -} - - -let cacheData = {}; -let cacheIndex = []; -let cacheSize = 0; -let maxCacheSize = 1024*1024 * 5; // 5MB - -function xhr (url, callback) { - if (cacheData[url]) { - if (callback) { - callback(cacheData[url]); - } - return; - } - - const req = new XMLHttpRequest(); - - req.onreadystatechange = function () { - if (req.readyState !== 4) { - return; - } - if (!req.status || req.status < 200 || req.status > 299) { - return; - } - if (callback && req.responseText) { - const responseText = req.responseText; - - cacheData[url] = responseText; - cacheIndex.push({ url: url, size: responseText.length }); - cacheSize += responseText.length; - - callback(responseText); - - while (cacheSize > maxCacheSize) { - let item = cacheIndex.shift(); - cacheSize -= item.size; - delete cacheData[item.url]; - } - } - }; - - req.open('GET', url); - req.send(null); - - return req; -} - -class Request { - - static loadJSON (url, callback) { - return xhr(url, responseText => { - let json; - try { - json = JSON.parse(responseText); - } catch(ex) {} - - callback(json); - }); - } -} - - -class Data { - - static getPixelFootprint (buffer) { - let footprint = new Int32Array(buffer.length), - px; - - for (let i = 0, il = buffer.length-1; i < il; i+=2) { - px = geoToPixel(buffer[i], buffer[i+1]); - footprint[i] = px.x; - footprint[i+1] = px.y; - } - - footprint = simplifyPolygon(footprint); - if (footprint.length < 8) { // 3 points & end==start (*2) - return; - } - - return footprint; - } - - static resetItems () { - this.items = []; - this.cache = {}; - Picking.reset(); - } - - static addRenderItems (data, allAreNew) { - let item, scaledItem, id; - let geojson = GeoJSON.read(data); - for (let i = 0, il = geojson.length; i < il; i++) { - item = geojson[i]; - id = item.id || [item.footprint[0], item.footprint[1], item.height, item.minHeight].join(','); - if (!this.cache[id]) { - if ((scaledItem = this.scaleItem(item))) { - scaledItem.scale = allAreNew ? 0 : 1; - this.items.push(scaledItem); - this.cache[id] = 1; - } - } - } - fadeIn(); - } - - static scalePolygon (buffer, factor) { - return buffer.map(coord => coord*factor); - } - - static scale (factor) { - Data.items = Data.items.map(item => { - // item.height = Math.min(item.height*factor, MAX_HEIGHT); // TODO: should be filtered by renderer - - item.height *= factor; - item.minHeight *= factor; - - item.footprint = Data.scalePolygon(item.footprint, factor); - item.center.x *= factor; - item.center.y *= factor; - - if (item.radius) { - item.radius *= factor; - } - - if (item.holes) { - for (let i = 0, il = item.holes.length; i < il; i++) { - item.holes[i] = Data.scalePolygon(item.holes[i], factor); - } - } - - item.roofHeight *= factor; - - return item; - }); - } - - static scaleItem (item) { - let - res = {}, - // TODO: calculate this on zoom change only - zoomScale = 6 / pow(2, ZOOM-MIN_ZOOM); // TODO: consider using HEIGHT / (devicePixelRatio || 1) - - if (item.id) { - res.id = item.id; - } - - res.height = min(item.height/zoomScale, MAX_HEIGHT); - - res.minHeight = isNaN(item.minHeight) ? 0 : item.minHeight / zoomScale; - if (res.minHeight > MAX_HEIGHT) { - return; - } - - res.footprint = this.getPixelFootprint(item.footprint); - if (!res.footprint) { - return; - } - res.center = getCenter(res.footprint); - - if (item.radius) { - res.radius = item.radius*PIXEL_PER_DEG; - } - if (item.shape) { - res.shape = item.shape; - } - if (item.roofShape) { - res.roofShape = item.roofShape; - } - if ((res.roofShape === 'cone' || res.roofShape === 'dome') && !res.shape && isRotational(res.footprint)) { - res.shape = 'cylinder'; - } - - if (item.holes) { - res.holes = []; - let innerFootprint; - for (let i = 0, il = item.holes.length; i < il; i++) { - // TODO: simplify - if ((innerFootprint = this.getPixelFootprint(item.holes[i]))) { - res.holes.push(innerFootprint); - } - } - } - - let color; - - if (item.wallColor) { - if ((color = Qolor.parse(item.wallColor))) { - res.altColor = ''+ color.lightness(0.8); - res.wallColor = ''+ color; - } - } - - if (item.roofColor) { - if ((color = Qolor.parse(item.roofColor))) { - res.roofColor = ''+ color; - } - } - - if (item.relationId) { - res.relationId = item.relationId; - } - res.hitColor = Picking.idToColor(item.relationId || item.id); - - res.roofHeight = isNaN(item.roofHeight) ? 0 : item.roofHeight/zoomScale; - - if (res.height+res.roofHeight <= res.minHeight) { - return; - } - - return res; - } - - static set (data) { - this.resetItems(); - this._staticData = data; - this.addRenderItems(this._staticData, true); - } - - static load (src, key) { - this.src = src || DATA_SRC.replace('{k}', (key || 'anonymous')); - this.update(); - } - - static update () { - this.resetItems(); - - if (ZOOM < MIN_ZOOM) { - return; - } - - if (this._staticData) { - this.addRenderItems(this._staticData); - } - - if (this.src) { - let - tileZoom = 16, - tileSize = 256, - zoomedTileSize = ZOOM > tileZoom ? tileSize << (ZOOM - tileZoom) : tileSize >> (tileZoom - ZOOM), - minX = ORIGIN_X / zoomedTileSize << 0, - minY = ORIGIN_Y / zoomedTileSize << 0, - maxX = ceil((ORIGIN_X + WIDTH) / zoomedTileSize), - maxY = ceil((ORIGIN_Y + HEIGHT) / zoomedTileSize), - x, y; - - let scope = this; - - function callback (json) { - scope.addRenderItems(json); - } - - for (y = minY; y <= maxY; y++) { - for (x = minX; x <= maxX; x++) { - this.loadTile(x, y, tileZoom, callback); - } - } - } - } - - static loadTile (x, y, zoom, callback) { - let s = 'abcd'[(x+y) % 4]; - let url = this.src.replace('{s}', s).replace('{x}', x).replace('{y}', y).replace('{z}', zoom); - return Request.loadJSON(url, callback); - } -} - -Data.cache = {}; // maintain a list of cached items in order to avoid duplicates on tile borders -Data.items = []; - -class Extrusion { - - static draw (context, polygon, innerPolygons, height, minHeight, color, altColor, roofColor) { - let - roof = this._extrude(context, polygon, height, minHeight, color, altColor), - innerRoofs = []; - - if (innerPolygons) { - for (let i = 0, il = innerPolygons.length; i < il; i++) { - innerRoofs[i] = this._extrude(context, innerPolygons[i], height, minHeight, color, altColor); - } - } - - context.fillStyle = roofColor; - - context.beginPath(); - this._ring(context, roof); - if (innerPolygons) { - for (let i = 0, il = innerRoofs.length; i < il; i++) { - this._ring(context, innerRoofs[i]); - } - } - context.closePath(); - context.fill(); - } - - static _extrude (context, polygon, height, minHeight, color, altColor) { - let - scale = CAM_Z / (CAM_Z-height), - minScale = CAM_Z / (CAM_Z-minHeight), - a = { x:0, y:0 }, - b = { x:0, y:0 }, - _a, _b, - roof = []; - - for (let i = 0, il = polygon.length-3; i < il; i += 2) { - a.x = polygon[i ]-ORIGIN_X; - a.y = polygon[i+1]-ORIGIN_Y; - b.x = polygon[i+2]-ORIGIN_X; - b.y = polygon[i+3]-ORIGIN_Y; - - _a = Buildings.project(a, scale); - _b = Buildings.project(b, scale); - - if (minHeight) { - a = Buildings.project(a, minScale); - b = Buildings.project(b, minScale); - } - - // backface culling check - if ((b.x-a.x) * (_a.y-a.y) > (_a.x-a.x) * (b.y-a.y)) { - // depending on direction, set wall shading - if ((a.x < b.x && a.y < b.y) || (a.x > b.x && a.y > b.y)) { - context.fillStyle = altColor; - } else { - context.fillStyle = color; - } - - context.beginPath(); - this._ring(context, [ - b.x, b.y, - a.x, a.y, - _a.x, _a.y, - _b.x, _b.y - ]); - context.closePath(); - context.fill(); - } - - roof[i] = _a.x; - roof[i+1] = _a.y; - } - - return roof; - } - - static _ring (context, polygon) { - context.moveTo(polygon[0], polygon[1]); - for (let i = 2, il = polygon.length-1; i < il; i += 2) { - context.lineTo(polygon[i], polygon[i+1]); - } - } - - static simplified (context, polygon, innerPolygons) { - context.beginPath(); - this._ringAbs(context, polygon); - if (innerPolygons) { - for (let i = 0, il = innerPolygons.length; i < il; i++) { - this._ringAbs(context, innerPolygons[i]); - } - } - context.closePath(); - context.fill(); - } - - static _ringAbs (context, polygon) { - context.moveTo(polygon[0]-ORIGIN_X, polygon[1]-ORIGIN_Y); - for (let i = 2, il = polygon.length-1; i < il; i += 2) { - context.lineTo(polygon[i]-ORIGIN_X, polygon[i+1]-ORIGIN_Y); - } - } - - static shadow (context, polygon, innerPolygons, height, minHeight) { - let - mode = null, - a = { x:0, y:0 }, - b = { x:0, y:0 }, - _a, _b; - - for (let i = 0, il = polygon.length-3; i < il; i += 2) { - a.x = polygon[i ]-ORIGIN_X; - a.y = polygon[i+1]-ORIGIN_Y; - b.x = polygon[i+2]-ORIGIN_X; - b.y = polygon[i+3]-ORIGIN_Y; - - _a = Shadows.project(a, height); - _b = Shadows.project(b, height); - - if (minHeight) { - a = Shadows.project(a, minHeight); - b = Shadows.project(b, minHeight); - } - - // mode 0: floor edges, mode 1: roof edges - if ((b.x-a.x) * (_a.y-a.y) > (_a.x-a.x) * (b.y-a.y)) { - if (mode === 1) { - context.lineTo(a.x, a.y); - } - mode = 0; - if (!i) { - context.moveTo(a.x, a.y); - } - context.lineTo(b.x, b.y); - } else { - if (mode === 0) { - context.lineTo(_a.x, _a.y); - } - mode = 1; - if (!i) { - context.moveTo(_a.x, _a.y); - } - context.lineTo(_b.x, _b.y); - } - } - - if (innerPolygons) { - for (let i = 0, il = innerPolygons.length; i < il; i++) { - this._ringAbs(context, innerPolygons[i]); - } - } - } - - static hitArea (context, polygon, innerPolygons, height, minHeight, color) { - let - mode = null, - a = { x:0, y:0 }, - b = { x:0, y:0 }, - scale = CAM_Z / (CAM_Z-height), - minScale = CAM_Z / (CAM_Z-minHeight), - _a, _b; - - context.fillStyle = color; - context.beginPath(); - - for (let i = 0, il = polygon.length-3; i < il; i += 2) { - a.x = polygon[i ]-ORIGIN_X; - a.y = polygon[i+1]-ORIGIN_Y; - b.x = polygon[i+2]-ORIGIN_X; - b.y = polygon[i+3]-ORIGIN_Y; - - _a = Buildings.project(a, scale); - _b = Buildings.project(b, scale); - - if (minHeight) { - a = Buildings.project(a, minScale); - b = Buildings.project(b, minScale); - } - - // mode 0: floor edges, mode 1: roof edges - if ((b.x-a.x) * (_a.y-a.y) > (_a.x-a.x) * (b.y-a.y)) { - if (mode === 1) { // mode is initially undefined - context.lineTo(a.x, a.y); - } - mode = 0; - if (!i) { - context.moveTo(a.x, a.y); - } - context.lineTo(b.x, b.y); - } else { - if (mode === 0) { // mode is initially undefined - context.lineTo(_a.x, _a.y); - } - mode = 1; - if (!i) { - context.moveTo(_a.x, _a.y); - } - context.lineTo(_b.x, _b.y); - } - } - - context.closePath(); - context.fill(); - } -} - -class Cylinder { - - static draw (context, center, radius, topRadius, height, minHeight, color, altColor, roofColor) { - let - c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y }, - scale = CAM_Z / (CAM_Z-height), - minScale = CAM_Z / (CAM_Z-minHeight), - apex = Buildings.project(c, scale), - a1, a2; - - topRadius *= scale; - - if (minHeight) { - c = Buildings.project(c, minScale); - radius = radius*minScale; - } - - // common tangents for ground and roof circle - let tangents = this._tangents(c, radius, apex, topRadius); - - // no tangents? top circle is inside bottom circle - if (!tangents) { - a1 = 1.5*PI; - a2 = 1.5*PI; - } else { - a1 = atan2(tangents[0].y1-c.y, tangents[0].x1-c.x); - a2 = atan2(tangents[1].y1-c.y, tangents[1].x1-c.x); - } - - context.fillStyle = color; - context.beginPath(); - context.arc(apex.x, apex.y, topRadius, HALF_PI, a1, true); - context.arc(c.x, c.y, radius, a1, HALF_PI); - context.closePath(); - context.fill(); - - context.fillStyle = altColor; - context.beginPath(); - context.arc(apex.x, apex.y, topRadius, a2, HALF_PI, true); - context.arc(c.x, c.y, radius, HALF_PI, a2); - context.closePath(); - context.fill(); - - context.fillStyle = roofColor; - this._circle(context, apex, topRadius); - } - - static simplified (context, center, radius) { - this._circle(context, { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y }, radius); - } - - static shadow (context, center, radius, topRadius, height, minHeight) { - let - c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y }, - apex = Shadows.project(c, height), - p1, p2; - - if (minHeight) { - c = Shadows.project(c, minHeight); - } - - // common tangents for ground and roof circle - let tangents = this._tangents(c, radius, apex, topRadius); - - // TODO: no tangents? roof overlaps everything near cam position - if (tangents) { - p1 = atan2(tangents[0].y1-c.y, tangents[0].x1-c.x); - p2 = atan2(tangents[1].y1-c.y, tangents[1].x1-c.x); - context.moveTo(tangents[1].x2, tangents[1].y2); - context.arc(apex.x, apex.y, topRadius, p2, p1); - context.arc(c.x, c.y, radius, p1, p2); - } else { - context.moveTo(c.x+radius, c.y); - context.arc(c.x, c.y, radius, 0, 2*PI); - } - } - - static hitArea (context, center, radius, topRadius, height, minHeight, color) { - let - c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y }, - scale = CAM_Z / (CAM_Z-height), - minScale = CAM_Z / (CAM_Z-minHeight), - apex = Buildings.project(c, scale), - p1, p2; - - topRadius *= scale; - - if (minHeight) { - c = Buildings.project(c, minScale); - radius = radius*minScale; - } - - // common tangents for ground and roof circle - let tangents = this._tangents(c, radius, apex, topRadius); - - context.fillStyle = color; - context.beginPath(); - - // TODO: no tangents? roof overlaps everything near cam position - if (tangents) { - p1 = atan2(tangents[0].y1-c.y, tangents[0].x1-c.x); - p2 = atan2(tangents[1].y1-c.y, tangents[1].x1-c.x); - context.moveTo(tangents[1].x2, tangents[1].y2); - context.arc(apex.x, apex.y, topRadius, p2, p1); - context.arc(c.x, c.y, radius, p1, p2); - } else { - context.moveTo(c.x+radius, c.y); - context.arc(c.x, c.y, radius, 0, 2*PI); - } - - context.closePath(); - context.fill(); - } - - static _circle (context, center, radius) { - context.beginPath(); - context.arc(center.x, center.y, radius, 0, PI*2); - context.fill(); - } - - // http://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Tangents_between_two_circles - static _tangents (c1, r1, c2, r2) { - let - dx = c1.x-c2.x, - dy = c1.y-c2.y, - dr = r1-r2, - sqdist = (dx*dx) + (dy*dy); - - if (sqdist <= dr*dr) { - return; - } - - let dist = sqrt(sqdist), - vx = -dx/dist, - vy = -dy/dist, - c = dr/dist, - res = [], - h, nx, ny; - - // Let A, B be the centers, and C, D be points at which the tangent - // touches first and second circle, and n be the normal vector to it. - // - // We have the system: - // n * n = 1 (n is a unit vector) - // C = A + r1 * n - // D = B + r2 * n - // n * CD = 0 (common orthogonality) - // - // n * CD = n * (AB + r2*n - r1*n) = AB*n - (r1 -/+ r2) = 0, <=> - // AB * n = (r1 -/+ r2), <=> - // v * n = (r1 -/+ r2) / d, where v = AB/|AB| = AB/d - // This is a linear equation in unknown vector n. - // Now we're just intersecting a line with a circle: v*n=c, n*n=1 - - h = sqrt(max(0, 1 - c*c)); - for (let sign = 1; sign >= -1; sign -= 2) { - nx = vx*c - sign*h*vy; - ny = vy*c + sign*h*vx; - res.push({ - x1: c1.x + r1*nx <<0, - y1: c1.y + r1*ny <<0, - x2: c2.x + r2*nx <<0, - y2: c2.y + r2*ny <<0 - }); - } - - return res; - } -} - -class Pyramid { - - static draw (context, polygon, center, height, minHeight, color, altColor) { - let - c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y }, - scale = CAM_Z / (CAM_Z-height), - minScale = CAM_Z / (CAM_Z-minHeight), - apex = Buildings.project(c, scale), - a = { x:0, y:0 }, - b = { x:0, y:0 }; - - for (let i = 0, il = polygon.length-3; i < il; i += 2) { - a.x = polygon[i ]-ORIGIN_X; - a.y = polygon[i+1]-ORIGIN_Y; - b.x = polygon[i+2]-ORIGIN_X; - b.y = polygon[i+3]-ORIGIN_Y; - - if (minHeight) { - a = Buildings.project(a, minScale); - b = Buildings.project(b, minScale); - } - - // backface culling check - if ((b.x-a.x) * (apex.y-a.y) > (apex.x-a.x) * (b.y-a.y)) { - // depending on direction, set shading - if ((a.x < b.x && a.y < b.y) || (a.x > b.x && a.y > b.y)) { - context.fillStyle = altColor; - } else { - context.fillStyle = color; - } - - context.beginPath(); - this._triangle(context, a, b, apex); - context.closePath(); - context.fill(); - } - } - } - - static _triangle (context, a, b, c) { - context.moveTo(a.x, a.y); - context.lineTo(b.x, b.y); - context.lineTo(c.x, c.y); - } - - static _ring (context, polygon) { - context.moveTo(polygon[0]-ORIGIN_X, polygon[1]-ORIGIN_Y); - for (let i = 2, il = polygon.length-1; i < il; i += 2) { - context.lineTo(polygon[i]-ORIGIN_X, polygon[i+1]-ORIGIN_Y); - } - } - - static shadow (context, polygon, center, height, minHeight) { - let - a = { x:0, y:0 }, - b = { x:0, y:0 }, - c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y }, - apex = Shadows.project(c, height); - - for (let i = 0, il = polygon.length-3; i < il; i += 2) { - a.x = polygon[i ]-ORIGIN_X; - a.y = polygon[i+1]-ORIGIN_Y; - b.x = polygon[i+2]-ORIGIN_X; - b.y = polygon[i+3]-ORIGIN_Y; - - if (minHeight) { - a = Shadows.project(a, minHeight); - b = Shadows.project(b, minHeight); - } - - // backface culling check - if ((b.x-a.x) * (apex.y-a.y) > (apex.x-a.x) * (b.y-a.y)) { - // depending on direction, set shading - this._triangle(context, a, b, apex); - } - } - } - - static hitArea (context, polygon, center, height, minHeight, color) { - let - c = { x:center.x-ORIGIN_X, y:center.y-ORIGIN_Y }, - scale = CAM_Z / (CAM_Z-height), - minScale = CAM_Z / (CAM_Z-minHeight), - apex = Buildings.project(c, scale), - a = { x:0, y:0 }, - b = { x:0, y:0 }; - - context.fillStyle = color; - context.beginPath(); - - for (let i = 0, il = polygon.length-3; i < il; i += 2) { - a.x = polygon[i ]-ORIGIN_X; - a.y = polygon[i+1]-ORIGIN_Y; - b.x = polygon[i+2]-ORIGIN_X; - b.y = polygon[i+3]-ORIGIN_Y; - - if (minHeight) { - a = Buildings.project(a, minScale); - b = Buildings.project(b, minScale); - } - - // backface culling check - if ((b.x-a.x) * (apex.y-a.y) > (apex.x-a.x) * (b.y-a.y)) { - this._triangle(context, a, b, apex); - } - } - - context.closePath(); - context.fill(); - } -} - -let animTimer; - -function fadeIn() { - if (animTimer) { - return; - } - - animTimer = setInterval(t => { - let dataItems = Data.items, - isNeeded = false; - - for (let i = 0, il = dataItems.length; i < il; i++) { - if (dataItems[i].scale < 1) { - dataItems[i].scale += 0.5*0.2; // amount*easing - if (dataItems[i].scale > 1) { - dataItems[i].scale = 1; - } - isNeeded = true; - } - } - - Layers.render(); - - if (!isNeeded) { - clearInterval(animTimer); - animTimer = null; - } - }, 33); -} - -class Layers { - - static init () { - Layers.container.className = 'osmb-container'; - - // TODO: improve this - Shadows.init(Layers.createContext(Layers.container)); - Simplified.init(Layers.createContext(Layers.container)); - Buildings.init(Layers.createContext(Layers.container)); - Picking.init(Layers.createContext()); - } - - static clear () { - Shadows.clear(); - Simplified.clear(); - Buildings.clear(); - Picking.clear(); - } - - static setOpacity (opacity) { - Shadows.setOpacity(opacity); - Simplified.setOpacity(opacity); - Buildings.setOpacity(opacity); - Picking.setOpacity(opacity); - } - - static render (quick) { - // show on high zoom levels only - if (ZOOM < MIN_ZOOM) { - Layers.clear(); - return; - } - - // don't render during zoom - if (IS_ZOOMING) { - return; - } - - requestAnimationFrame(f => { - if (!quick) { - Shadows.render(); - Simplified.render(); - //HitAreas.render(); // TODO: do this on demand - } - Buildings.render(); - }); - } - - static createContext (container) { - let canvas = document.createElement('CANVAS'); - canvas.className = 'osmb-layer'; - - let context = canvas.getContext('2d'); - context.lineCap = 'round'; - context.lineJoin = 'round'; - context.lineWidth = 1; - context.imageSmoothingEnabled = false; - - Layers.items.push(canvas); - if (container) { - container.appendChild(canvas); - } - - return context; - } - - static appendTo (parentNode) { - parentNode.appendChild(Layers.container); - } - - static remove () { - Layers.container.parentNode.removeChild(Layers.container); - } - - static setSize (width, height) { - Layers.items.forEach(canvas => { - canvas.width = width; - canvas.height = height; - }); - } - - // usually called after move: container jumps by move delta, cam is reset - static setPosition (x, y) { - Layers.container.style.left = x +'px'; - Layers.container.style.top = y +'px'; - } -} - -Layers.container = document.createElement('DIV'); -Layers.items = []; - -class Buildings { - - static init (context) { - this.context = context; - } - - static clear () { - this.context.clearRect(0, 0, WIDTH, HEIGHT); - } - - static setOpacity (opacity) { - this.context.canvas.style.opacity = opacity; - } - - static project (p, m) { - return { - x: (p.x-CAM_X) * m + CAM_X <<0, - y: (p.y-CAM_Y) * m + CAM_Y <<0 - }; - } - - static render () { - this.clear(); - - let - context = this.context, - item, - h, mh, - sortCam = { x:CAM_X+ORIGIN_X, y:CAM_Y+ORIGIN_Y }, - footprint, - wallColor, altColor, roofColor, - dataItems = Data.items; - - dataItems.sort((a, b) => { - return (a.minHeight-b.minHeight) || getDistance(b.center, sortCam) - getDistance(a.center, sortCam) || (b.height-a.height); - }); - - for (let i = 0, il = dataItems.length; i < il; i++) { - item = dataItems[i]; - - if (Simplified.isSimple(item)) { - continue; - } - - footprint = item.footprint; - - if (!isVisible(footprint)) { - continue; - } - - // when fading in, use a dynamic height - h = item.scale < 1 ? item.height*item.scale : item.height; - - mh = 0; - if (item.minHeight) { - mh = item.scale < 1 ? item.minHeight*item.scale : item.minHeight; - } - - wallColor = item.wallColor || WALL_COLOR_STR; - altColor = item.altColor || ALT_COLOR_STR; - roofColor = item.roofColor || ROOF_COLOR_STR; - context.strokeStyle = altColor; - - switch (item.shape) { - case 'cylinder': Cylinder.draw(context, item.center, item.radius, item.radius, h, mh, wallColor, altColor, roofColor); break; - case 'cone': Cylinder.draw(context, item.center, item.radius, 0, h, mh, wallColor, altColor); break; - case 'dome': Cylinder.draw(context, item.center, item.radius, item.radius/2, h, mh, wallColor, altColor); break; - case 'sphere': Cylinder.draw(context, item.center, item.radius, item.radius, h, mh, wallColor, altColor, roofColor); break; - case 'pyramid': Pyramid.draw(context, footprint, item.center, h, mh, wallColor, altColor); break; - default: Extrusion.draw(context, footprint, item.holes, h, mh, wallColor, altColor, roofColor); - } - - switch (item.roofShape) { - case 'cone': Cylinder.draw(context, item.center, item.radius, 0, h+item.roofHeight, h, roofColor, ''+ Qolor.parse(roofColor).lightness(0.9)); break; - case 'dome': Cylinder.draw(context, item.center, item.radius, item.radius/2, h+item.roofHeight, h, roofColor, ''+ Qolor.parse(roofColor).lightness(0.9)); break; - case 'pyramid': Pyramid.draw(context, footprint, item.center, h+item.roofHeight, h, roofColor, Qolor.parse(roofColor).lightness(0.9)); break; - } - } - } -} - -class Simplified { - - static init (context) { - this.context = context; - } - - static clear () { - this.context.clearRect(0, 0, WIDTH, HEIGHT); - } - - static setOpacity (opacity) { - this.context.canvas.style.opacity = opacity; - } - - static isSimple (item) { - return (ZOOM <= Simplified.MAX_ZOOM && item.height+item.roofHeight < Simplified.MAX_HEIGHT); - } - - static render () { - this.clear(); - - let context = this.context; - - // show on high zoom levels only and avoid rendering during zoom - if (ZOOM > Simplified.MAX_ZOOM) { - return; - } - - let - item, - footprint, - dataItems = Data.items; - - for (let i = 0, il = dataItems.length; i < il; i++) { - item = dataItems[i]; - - if (item.height >= Simplified.MAX_HEIGHT) { - continue; - } - - footprint = item.footprint; - - if (!isVisible(footprint)) { - continue; - } - - context.strokeStyle = item.altColor || ALT_COLOR_STR; - context.fillStyle = item.roofColor || ROOF_COLOR_STR; - - switch (item.shape) { - case 'cylinder': - case 'cone': - case 'dome': - case 'sphere': Cylinder.simplified(context, item.center, item.radius); break; - default: Extrusion.simplified(context, footprint, item.holes); - } - } - } -} - -Simplified.MAX_ZOOM = 16; // max zoom where buildings could render simplified -Simplified.MAX_HEIGHT = 5; // max building height in order to be simple - -class Shadows { - - static init (context) { - this.context = context; - } - - static clear () { - this.context.clearRect(0, 0, WIDTH, HEIGHT); - } - - static setOpacity (opacity) { - this.opacity = opacity; - } - - static project (p, h) { - return { - x: p.x + this.direction.x*h, - y: p.y + this.direction.y*h - }; - } - - static render () { - this.clear(); - - let - context = this.context, - screenCenter, - sun, length, alpha; - - // TODO: calculate this just on demand - screenCenter = pixelToGeo(CENTER_X+ORIGIN_X, CENTER_Y+ORIGIN_Y); - sun = getSunPosition(this.date, screenCenter.latitude, screenCenter.longitude); - - if (sun.altitude <= 0) { - return; - } - - length = 1 / tan(sun.altitude); - alpha = length < 5 ? 0.75 : 1/length*5; - - this.direction.x = cos(sun.azimuth) * length; - this.direction.y = sin(sun.azimuth) * length; - - let - i, il, - item, - h, mh, - footprint, - dataItems = Data.items; - - context.canvas.style.opacity = alpha / (this.opacity * 2); - context.shadowColor = this.blurColor; - context.fillStyle = this.color; - context.beginPath(); - - for (i = 0, il = dataItems.length; i < il; i++) { - item = dataItems[i]; - - footprint = item.footprint; - - if (!isVisible(footprint)) { - continue; - } - - // when fading in, use a dynamic height - h = item.scale < 1 ? item.height*item.scale : item.height; - - mh = 0; - if (item.minHeight) { - mh = item.scale < 1 ? item.minHeight*item.scale : item.minHeight; - } - - switch (item.shape) { - case 'cylinder': Cylinder.shadow(context, item.center, item.radius, item.radius, h, mh); break; - case 'cone': Cylinder.shadow(context, item.center, item.radius, 0, h, mh); break; - case 'dome': Cylinder.shadow(context, item.center, item.radius, item.radius/2, h, mh); break; - case 'sphere': Cylinder.shadow(context, item.center, item.radius, item.radius, h, mh); break; - case 'pyramid': Pyramid.shadow(context, footprint, item.center, h, mh); break; - default: Extrusion.shadow(context, footprint, item.holes, h, mh); - } - - switch (item.roofShape) { - case 'cone': Cylinder.shadow(context, item.center, item.radius, 0, h+item.roofHeight, h); break; - case 'dome': Cylinder.shadow(context, item.center, item.radius, item.radius/2, h+item.roofHeight, h); break; - case 'pyramid': Pyramid.shadow(context, footprint, item.center, h+item.roofHeight, h); break; - } - } - - context.closePath(); - context.fill(); - } -} - -Shadows.color = '#666666'; -Shadows.blurColor = '#000000'; -Shadows.date = new Date(); -Shadows.direction = { x:0, y:0 }; -Shadows.opacity = 1; - - - -class Picking { - - static init (context) { - this.context = context; - } - - static setOpacity (opacity) {} - - static clear () {} - - static reset () { - this._idMapping = [null]; - } - - static render () { - if (this._timer) { - return; - } - let self = this; - this._timer = setTimeout(t => { - self._timer = null; - self._render(); - }, 500); - } - - static _render () { - this.clear(); - - let - context = this.context, - item, - h, mh, - sortCam = { x:CAM_X+ORIGIN_X, y:CAM_Y+ORIGIN_Y }, - footprint, - color, - dataItems = Data.items; - - dataItems.sort((a, b) => { - return (a.minHeight-b.minHeight) || getDistance(b.center, sortCam) - getDistance(a.center, sortCam) || (b.height-a.height); - }); - - for (let i = 0, il = dataItems.length; i < il; i++) { - item = dataItems[i]; - - if (!(color = item.hitColor)) { - continue; - } - - footprint = item.footprint; - - if (!isVisible(footprint)) { - continue; - } - - h = item.height; - - mh = 0; - if (item.minHeight) { - mh = item.minHeight; - } - - switch (item.shape) { - case 'cylinder': Cylinder.hitArea(context, item.center, item.radius, item.radius, h, mh, color); break; - case 'cone': Cylinder.hitArea(context, item.center, item.radius, 0, h, mh, color); break; - case 'dome': Cylinder.hitArea(context, item.center, item.radius, item.radius/2, h, mh, color); break; - case 'sphere': Cylinder.hitArea(context, item.center, item.radius, item.radius, h, mh, color); break; - case 'pyramid': Pyramid.hitArea(context, footprint, item.center, h, mh, color); break; - default: Extrusion.hitArea(context, footprint, item.holes, h, mh, color); - } - - switch (item.roofShape) { - case 'cone': Cylinder.hitArea(context, item.center, item.radius, 0, h+item.roofHeight, h, color); break; - case 'dome': Cylinder.hitArea(context, item.center, item.radius, item.radius/2, h+item.roofHeight, h, color); break; - case 'pyramid': Pyramid.hitArea(context, footprint, item.center, h+item.roofHeight, h, color); break; - } - } - - // otherwise fails on size 0 - if (WIDTH && HEIGHT) { - this._imageData = this.context.getImageData(0, 0, WIDTH, HEIGHT).data; - } - } - - static getIdFromXY (x, y) { - let imageData = this._imageData; - if (!imageData) { - return; - } - let pos = 4*((y|0) * WIDTH + (x|0)); - let index = imageData[pos] | (imageData[pos+1]<<8) | (imageData[pos+2]<<16); - return this._idMapping[index]; - } - - static idToColor (id) { - let index = this._idMapping.indexOf(id); - if (index === -1) { - this._idMapping.push(id); - index = this._idMapping.length-1; - } - let r = index & 0xff; - let g = (index >>8) & 0xff; - let b = (index >>16) & 0xff; - return 'rgb('+ [r, g, b].join(',') +')'; - } -} - -Picking._idMapping = [null]; - - -class Debug { - - static point (x, y, color, size) { - const context = this.context; - context.fillStyle = color || '#ffcc00'; - context.beginPath(); - context.arc(x, y, size || 3, 0, 2*PI); - context.closePath(); - context.fill(); - } - - static line (ax, ay, bx, by, color) { - const context = this.context; - context.strokeStyle = color || '#ffcc00'; - context.beginPath(); - context.moveTo(ax, ay); - context.lineTo(bx, by); - context.closePath(); - context.stroke(); - } -} - - -function setOrigin (origin) { - ORIGIN_X = origin.x; - ORIGIN_Y = origin.y; -} - -function moveCam (offset) { - CAM_X = CENTER_X + offset.x; - CAM_Y = HEIGHT + offset.y; - Layers.render(true); -} - -function setSize (size) { - WIDTH = size.width; - HEIGHT = size.height; - CENTER_X = WIDTH /2 <<0; - CENTER_Y = HEIGHT/2 <<0; - - CAM_X = CENTER_X; - CAM_Y = HEIGHT; - - Layers.setSize(WIDTH, HEIGHT); - MAX_HEIGHT = CAM_Z-50; -} - -function setZoom (z) { - ZOOM = z; - MAP_SIZE = MAP_TILE_SIZE < fadeIn() => Layers.render() -} - -function onZoomStart () { - IS_ZOOMING = true; -} - -function onZoomEnd (e) { - IS_ZOOMING = false; - const factor = Math.pow(2, e.zoom-ZOOM); - - setZoom(e.zoom); - // Layers.render(); // TODO: requestAnimationFrame() causes flickering because layers are already cleared - - // show on high zoom levels only - if (ZOOM <= MIN_ZOOM) { - Layers.clear(); - return; - } - - Data.scale(factor); - - Shadows.render(); - Simplified.render(); - Buildings.render(); - - Data.update(); // => fadeIn() -} - - -class OSMBuildings extends L.Layer { - - constructor (map) { - super(map); - - this.offset = {x: 0, y: 0}; - Layers.init(); - if (map) { - map.addLayer(this); - } - } - - addTo (map) { - map.addLayer(this); - return this; - } - - onAdd (map) { - this.map = map; - Layers.appendTo(map._panes.overlayPane); - - let - off = this.getOffset(), - po = map.getPixelOrigin(); - setSize({width: map._size.x, height: map._size.y}); - setOrigin({x: po.x - off.x, y: po.y - off.y}); - setZoom(map._zoom); - - Layers.setPosition(-off.x, -off.y); - - map.on({ - move: this.onMove, - moveend: this.onMoveEnd, - zoomstart: this.onZoomStart, - zoomend: this.onZoomEnd, - resize: this.onResize, - viewreset: this.onViewReset, - click: this.onClick - }, this); - - if (map.options.zoomAnimation) { - map.on('zoomanim', this.onZoom, this); - } - - if (map.attributionControl) { - map.attributionControl.addAttribution(ATTRIBUTION); - } - - Data.update(); - } - - onRemove () { - let map = this.map; - if (map.attributionControl) { - map.attributionControl.removeAttribution(ATTRIBUTION); - } - - map.off({ - move: this.onMove, - moveend: this.onMoveEnd, - zoomstart: this.onZoomStart, - zoomend: this.onZoomEnd, - resize: this.onResize, - viewreset: this.onViewReset, - click: this.onClick - }, this); - - if (map.options.zoomAnimation) { - map.off('zoomanim', this.onZoom, this); - } - Layers.remove(); - map = null; - } - - onMove (e) { - let off = this.getOffset(); - moveCam({x: this.offset.x - off.x, y: this.offset.y - off.y}); - } - - onMoveEnd (e) { - if (this.noMoveEnd) { // moveend is also fired after zoom - this.noMoveEnd = false; - return; - } - - let - map = this.map, - off = this.getOffset(), - po = map.getPixelOrigin(); - - this.offset = off; - Layers.setPosition(-off.x, -off.y); - moveCam({x: 0, y: 0}); - - setSize({width: map._size.x, height: map._size.y}); // in case this is triggered by resize - setOrigin({x: po.x - off.x, y: po.y - off.y}); - onMoveEnd(e); - } - - onZoomStart (e) { - onZoomStart(e); - } - - onZoom (e) { - let center = this.map.latLngToContainerPoint(e.center); - let scale = Math.pow(2, e.zoom - ZOOM); - - let dx = WIDTH / 2 - center.x; - let dy = HEIGHT / 2 - center.y; - - let x = WIDTH / 2; - let y = HEIGHT / 2; - - if (e.zoom > ZOOM) { - x -= dx * scale; - y -= dy * scale; - } else { - x += dx; - y += dy; - } - - Layers.container.classList.add('zoom-animation'); - Layers.container.style.transformOrigin = x + 'px ' + y + 'px'; - Layers.container.style.transform = 'translate3d(0, 0, 0) scale(' + scale + ')'; - } - - onZoomEnd (e) { - Layers.clear(); - Layers.container.classList.remove('zoom-animation'); - Layers.container.style.transform = 'translate3d(0, 0, 0) scale(1)'; - - let - map = this.map, - off = this.getOffset(), - po = map.getPixelOrigin(); - - setOrigin({x: po.x - off.x, y: po.y - off.y}); - onZoomEnd({zoom: map._zoom}); - this.noMoveEnd = true; - } - - onResize () { - } - - onViewReset () { - let off = this.getOffset(); - - this.offset = off; - Layers.setPosition(-off.x, -off.y); - moveCam({x: 0, y: 0}); - } - - onClick (e) { - let id = Picking.getIdFromXY(e.containerPoint.x, e.containerPoint.y); - if (id) { - onClick({feature: id, lat: e.latlng.lat, lon: e.latlng.lng}); - } - } - - getOffset () { - return L.DomUtil.getPosition(this.map._mapPane); - } - - //*** COMMON PUBLIC METHODS *** - - style (style) { - style = style || {}; - let color; - if ((color = style.color || style.wallColor)) { - WALL_COLOR = Qolor.parse(color); - WALL_COLOR_STR = '' + WALL_COLOR; - - ALT_COLOR = WALL_COLOR.lightness(0.8); - ALT_COLOR_STR = '' + ALT_COLOR; - - ROOF_COLOR = WALL_COLOR.lightness(1.2); - ROOF_COLOR_STR = '' + ROOF_COLOR; - } - - if (style.roofColor) { - ROOF_COLOR = Qolor.parse(style.roofColor); - ROOF_COLOR_STR = '' + ROOF_COLOR; - } - - Layers.render(); - - return this; - } - - date (date) { - Shadows.date = date; - Shadows.render(); - return this; - } - - load (url) { - Data.load(url); - return this; - } - - set (data) { - Data.set(data); - return this; - } - - each (handler) { - onEach = function (payload) { - return handler(payload); - }; - return this; - } - - click (handler) { - onClick = function (payload) { - return handler(payload); - }; - return this; - } -} - -OSMBuildings.VERSION = VERSION; -OSMBuildings.ATTRIBUTION = ATTRIBUTION; - - return OSMBuildings; -}()); diff --git a/inst/htmlwidgets/lfx-building/osm-buildings-bindings.js b/inst/htmlwidgets/lfx-building/osm-buildings-bindings.js index c530b252..db1f5853 100644 --- a/inst/htmlwidgets/lfx-building/osm-buildings-bindings.js +++ b/inst/htmlwidgets/lfx-building/osm-buildings-bindings.js @@ -1,46 +1,76 @@ -LeafletWidget.methods.addBuilding = function(layerId, group, opacity, attribution) { -// (function(){ +LeafletWidget.methods.addBuilding = function(buildingURL, group, eachFn, clickFn, data) { + (function(){ var map = this; if (map.osmb) { map.osmb.remove(); delete map.osmb; } - console.log(("Schaff ich es hjier")) - - map.setView([52.51836, 13.40438], 16, false); - - new L.TileLayer('https://tile-a.openstreetmap.fr/hot/{z}/{x}/{y}.png', { - attribution: '© Data OpenStreetMap', - maxZoom: 18, - maxNativeZoom: 20 - }).addTo(map); var osmb = new OSMBuildings(map) - .load('https://{s}.data.osmbuildings.org/0.2/59fcc2e8/tile/{z}/{x}/{y}.json'); + .date(new Date()) + if (data) { + if (data.features && data.features[0].properties.height && data.features[0].geometry.type == "Polygon") { + console.log("data is defined"); console.log(data); + osmb.set(data); + } else { + console.error("The data is not a correct GeoJSON of type 'Polygon' or has no property 'height'."); + } + } else { + osmb.load(buildingURL); + } - osmb.date(new Date(2017, 15, 1, 19, 30)) + if (eachFn && typeof eachFn === 'function') { + osmb.each(eachFn); + } + if (clickFn && typeof clickFn === 'function') { + osmb.click(clickFn); + } + map.layerManager.addLayer(osmb, "building", null, group); map.osmb = osmb; -// }).call(this); + }).call(this); }; LeafletWidget.methods.updateBuildingTime = function(date) { - var map = this; - if (map.osmb) { - var now = new Date(date); - console.log("now"); console.log(now) - var Y = now.getFullYear(), - M = now.getMonth(), - D = now.getDate(), - h = now.getHours(), - m = now.getMinutes(); - - // Update the date on the OSMBuildings instance - map.osmb.date(new Date(Y, M, D, h, m)); - } else { - console.error("OSMBuildings instance is not initialized."); - } + (function(){ + var map = this; + if (map.osmb) { + var now = new Date(date); + var Y = now.getFullYear(), + M = now.getMonth(), + D = now.getDate(), + h = now.getHours(), + m = now.getMinutes(); + + // Update the date on the OSMBuildings instance + map.osmb.date(new Date(Y, M, D, h, m)); + } else { + console.error("OSMBuildings instance is not initialized."); + } + }).call(this); +}; + +LeafletWidget.methods.setBuildingStyle = function(style) { + (function(){ + var map = this; + if (map.osmb) { + map.osmb.style(style); + } else { + console.error("OSMBuildings instance is not initialized."); + } + }).call(this); +}; + +LeafletWidget.methods.setBuildingData = function(data) { + (function(){ + var map = this; + if (map.osmb) { + map.osmb.set(data); + } else { + console.error("OSMBuildings instance is not initialized."); + } + }).call(this); }; diff --git a/inst/htmlwidgets/lfx-building/osm-buildings.css b/inst/htmlwidgets/lfx-building/osm-buildings.css index c656a9ee..e3b5a5ae 100644 --- a/inst/htmlwidgets/lfx-building/osm-buildings.css +++ b/inst/htmlwidgets/lfx-building/osm-buildings.css @@ -1,3 +1,6 @@ +/* + +*/ .osmb-container { transform: translate3d(0, 0, 0); pointerEvents: none; @@ -19,3 +22,4 @@ left: 0; top: 0; } + diff --git a/man/addBuildings.Rd b/man/addBuildings.Rd index 97a5fcea..56ba1b86 100644 --- a/man/addBuildings.Rd +++ b/man/addBuildings.Rd @@ -2,33 +2,56 @@ % Please edit documentation in R/buildings.R \name{addBuildings} \alias{addBuildings} -\title{Add OSM-Buildings} +\title{Add OSM-Buildings to a Leaflet Map} \usage{ addBuildings( map, - layerId = NULL, + buildingURL = "https://{s}.data.osmbuildings.org/0.2/59fcc2e8/tile/{z}/{x}/{y}.json", group = NULL, - opacity = 0.5, - attribution = "© Map tiles Mapbox" + eachFn = NULL, + clickFn = NULL, + data = NULL ) } \arguments{ -\item{map}{A map widget object created from \code{\link[leaflet]{leaflet}}} +\item{map}{A map widget object created from \code{\link[leaflet]{leaflet}}.} -\item{options}{List of further options. See \code{\link{hexbinOptions}}} +\item{buildingURL}{The URL template for the building data. Default is the OSM Buildings tile server: \cr +\code{"https://{s}.data.osmbuildings.org/0.2/59fcc2e8/tile/{z}/{x}/{y}.json"}.} + +\item{group}{The name of the group the buildings will be added to.} + +\item{eachFn}{A JavaScript function (using \code{\link[htmlwidgets]{JS}}) that will be called for each building feature. Use this to apply custom logic to each feature.} + +\item{clickFn}{A JavaScript function (using \code{\link[htmlwidgets]{JS}}) that will be called when a building is clicked. Use this to handle click events on buildings.} + +\item{data}{A GeoJSON object containing Polygon features representing the buildings. The properties of these polygons can include attributes like \code{height}, \code{color}, \code{roofColor}, and others as specified in the OSM Buildings documentation.} } \description{ -Add OSM-Buildings +This function adds 2.5D buildings to a Leaflet map using the OSM Buildings plugin. +} +\details{ +The `data` parameter allows you to provide custom building data as a GeoJSON object. The following properties can be used within the GeoJSON: +\itemize{ + \item \strong{height} + \item \strong{minHeight} + \item \strong{color/wallColor} + \item \strong{material} + \item \strong{roofColor} + \item \strong{roofMaterial} + \item \strong{shape} + \item \strong{roofShape} + \item \strong{roofHeight} } -\note{ -Out of the box a legend image is only available for Pressure, - Precipitation Classic, Clouds Classic, Rain Classic, Snow, Temperature and - Wind Speed. + +See the OSM Wiki: \href{https://wiki.openstreetmap.org/wiki/Simple_3D_Buildings} } \seealso{ -https://osmbuildings.org/documentation/viewer/ +\url{https://github.com/kekscom/osmbuildings/} for more details on the OSM Buildings plugin and available properties. Other OSM-Buildings Plugin: +\code{\link{setBuildingData}()}, +\code{\link{setBuildingStyle}()}, \code{\link{updateBuildingTime}()} } \concept{OSM-Buildings Plugin} diff --git a/man/setBuildingData.Rd b/man/setBuildingData.Rd new file mode 100644 index 00000000..2d720884 --- /dev/null +++ b/man/setBuildingData.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/buildings.R +\name{setBuildingData} +\alias{setBuildingData} +\title{Update the OSM-Buildings Data} +\usage{ +setBuildingData(map, data) +} +\arguments{ +\item{map}{A map widget object created from \code{\link[leaflet]{leaflet}}.} + +\item{data}{A GeoJSON object containing Polygon features representing the buildings. The properties of these polygons can include attributes like \code{height}, \code{color}, \code{roofColor}, and others as specified in the OSM Buildings documentation.} +} +\description{ +Update the OSM-Buildings Data +} +\seealso{ +Other OSM-Buildings Plugin: +\code{\link{addBuildings}()}, +\code{\link{setBuildingStyle}()}, +\code{\link{updateBuildingTime}()} +} +\concept{OSM-Buildings Plugin} diff --git a/man/setBuildingStyle.Rd b/man/setBuildingStyle.Rd new file mode 100644 index 00000000..4e10a510 --- /dev/null +++ b/man/setBuildingStyle.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/buildings.R +\name{setBuildingStyle} +\alias{setBuildingStyle} +\title{Update the OSM-Buildings Style} +\usage{ +setBuildingStyle( + map, + style = list(color = "#ffcc00", wallColor = "#ffcc00", roofColor = "orange", shadows = + TRUE) +) +} +\arguments{ +\item{map}{A map widget object created from \code{\link[leaflet]{leaflet}}.} + +\item{style}{A named list of styles} +} +\description{ +Update the OSM-Buildings Style +} +\seealso{ +Other OSM-Buildings Plugin: +\code{\link{addBuildings}()}, +\code{\link{setBuildingData}()}, +\code{\link{updateBuildingTime}()} +} +\concept{OSM-Buildings Plugin} diff --git a/man/updateBuildingTime.Rd b/man/updateBuildingTime.Rd index 2897a111..f5770b5f 100644 --- a/man/updateBuildingTime.Rd +++ b/man/updateBuildingTime.Rd @@ -7,15 +7,17 @@ updateBuildingTime(map, time) } \arguments{ -\item{map}{A map widget object created from \code{\link[leaflet]{leaflet}}} +\item{map}{A map widget object created from \code{\link[leaflet]{leaflet}}.} + +\item{time}{a timestamp that can be converted to POSIXct} } \description{ Update the Shadows OSM-Buildings with a POSIXct timestamp } \seealso{ -https://osmbuildings.org/documentation/viewer/ - Other OSM-Buildings Plugin: -\code{\link{addBuildings}()} +\code{\link{addBuildings}()}, +\code{\link{setBuildingData}()}, +\code{\link{setBuildingStyle}()} } \concept{OSM-Buildings Plugin} diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 00000000..fb2bfdf9 --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,12 @@ +# This file is part of the standard setup for testthat. +# It is recommended that you do not modify it. +# +# Where should you do additional test configuration? +# Learn more about the roles of various files in: +# * https://r-pkgs.org/testing-design.html#sec-tests-files-overview +# * https://testthat.r-lib.org/articles/special-files.html + +library(testthat) +library(leaflet.extras2) + +test_check("leaflet.extras2") diff --git a/tests/testthat/test-osmbuildings.R b/tests/testthat/test-osmbuildings.R new file mode 100644 index 00000000..55569af4 --- /dev/null +++ b/tests/testthat/test-osmbuildings.R @@ -0,0 +1,148 @@ +# library(testthat) +# library(leaflet) + +create_test_map <- function() { + leaflet() %>% addTiles() +} + +# Test suite for addBuildings +test_that("addBuildings adds dependencies and invokes method correctly", { + map <- create_test_map() + + # Call addBuildings without additional arguments + map <- addBuildings(map) + + # Check if the dependencies are added + expect_true(any(sapply(map$dependencies, function(dep) dep$name) == "lfx-building")) + + # Check if invokeMethod is called with correct arguments + expect_equal(map$x$calls[[2]]$method, "addBuilding") + expect_equal(map$x$calls[[2]]$args[[1]], "https://{s}.data.osmbuildings.org/0.2/59fcc2e8/tile/{z}/{x}/{y}.json") +}) + +test_that("addBuildings handles custom eachFn, clickFn, and data", { + map <- create_test_map() + + # Define custom JavaScript functions using htmlwidgets::JS + each_fn <- htmlwidgets::JS("function(e) { console.log('each:', e); }") + click_fn <- htmlwidgets::JS("function(e) { console.log('click:', e); }") + + # Define custom GeoJSON data + geojson_data <- list( + type = "FeatureCollection", + features = list( + list( + type = "Feature", + properties = list(height = 100, color = "#ff0000"), + geometry = list( + type = "Polygon", + coordinates = list( + list( + c(13.39631974697113, 52.52184840804295), + c(13.39496523141861, 52.521166220963536), + c(13.395150303840637, 52.52101770514734), + c(13.396652340888977, 52.52174559105107), + c(13.39631974697113, 52.52184840804295) + ) + ) + ) + ) + ) + ) + + map <- addBuildings(map, eachFn = each_fn, clickFn = click_fn, data = geojson_data) + + # Check if the JavaScript functions and data are passed correctly + expect_equal(map$x$calls[[2]]$args[[3]], each_fn) + expect_equal(map$x$calls[[2]]$args[[4]], click_fn) + expect_equal(map$x$calls[[2]]$args[[5]], geojson_data) +}) + +# Test suite for updateBuildingTime +test_that("updateBuildingTime updates the time correctly", { + map <- create_test_map() + time <- Sys.time() + + map <- addBuildings(map) %>% + updateBuildingTime(time) %>% + setView(13.40, 52.51836,15) + + # Check if invokeMethod is called with the correct timestamp + expect_equal(map$x$calls[[3]]$method, "updateBuildingTime") + expect_equal(map$x$calls[[3]]$args[[1]], time) +}) + +# Test suite for setBuildingStyle +test_that("setBuildingStyle applies styles correctly", { + map <- create_test_map() + style <- list(color = "#0000ff", wallColor = "#0000ff", roofColor = "blue", shadows = FALSE) + + map <- addBuildings(map) %>% + setBuildingStyle(style) %>% + setView(13.40, 52.51836,15) + + # Check if invokeMethod is called with the correct style + expect_equal(map$x$calls[[3]]$method, "setBuildingStyle") + expect_equal(map$x$calls[[3]]$args[[1]], style) +}) + +test_that("setBuildingStyle uses default styles if not provided", { + map <- create_test_map() + + map <- addBuildings(map) %>% + setBuildingStyle() %>% + setView(13.40, 52.51836,15) + # map + + # Check if invokeMethod is called with the default styles + default_style <- list(color = "#ffcc00", wallColor = "#ffcc00", roofColor = "orange", shadows = TRUE) + expect_equal(map$x$calls[[3]]$"method", "setBuildingStyle") + expect_equal(map$x$calls[[3]]$args[[1]], default_style) +}) + +# Test suite for setBuildingData +test_that("setBuildingData updates the building data correctly", { + map <- create_test_map() + + # Define custom GeoJSON data + geojson_data <- list( + type = "FeatureCollection", + features = list( + list( + type = "Feature", + properties = list(height = 100, color = "#ff0000"), + geometry = list( + type = "Polygon", + coordinates = list( + list( + c(13.39631974697113, 52.52184840804295), + c(13.39496523141861, 52.521166220963536), + c(13.395150303840637, 52.52101770514734), + c(13.396652340888977, 52.52174559105107), + c(13.39631974697113, 52.52184840804295) + ) + ) + ) + ) + ) + ) + + map <- addBuildings(map, + buildingURL = NULL, + data = geojson_data) %>% + setView(13.40, 52.51836,15) + # map + # Check if invokeMethod is called with the correct data + expect_equal(map$x$calls[[2]]$method, "addBuilding") + expect_equal(map$x$calls[[2]]$args[[5]], geojson_data) + + map <- addBuildings(create_test_map(), buildingURL = NULL) %>% + setBuildingData(geojson_data) %>% + setView(13.40, 52.51836,15) + # map + # Check if invokeMethod is called with the correct data + expect_equal(map$x$calls[[2]]$method, "addBuilding") + expect_true(is.null(unlist(map$x$calls[[2]]$args))) + expect_equal(map$x$calls[[3]]$method, "setBuildingData") + expect_equal(map$x$calls[[3]]$args[[1]], geojson_data) +})