diff --git a/.gitignore b/.gitignore index 5148e52..344e998 100644 --- a/.gitignore +++ b/.gitignore @@ -1,37 +1,7 @@ -# Logs -logs -*.log -npm-debug.log* - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules -jspm_packages - -# Optional npm cache directory -.npm - -# Optional REPL history -.node_repl_history +/bower_components/ +/node_modules/ +/.pulp-cache/ +/output/ +/example/index.js +/.psc* +/.psa* diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..cd72472 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +language: node_js +dist: trusty +sudo: required +node_js: stable +install: + - npm install + - npm install -g bower + - bower install +script: + - npm run build:browser +after_success: +- >- + test $TRAVIS_TAG && + echo $GITHUB_TOKEN | pulp login && + echo y | pulp publish --no-push diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..3b76200 --- /dev/null +++ b/bower.json @@ -0,0 +1,24 @@ +{ + "name": "purescript-leafletjs-halogen", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "output" + ], + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git://github.com/slamdata/purescript-leafletjs-halogen.git" + }, + "dependencies": { + "purescript-eff": "^3.1.0", + "purescript-halogen": "^2.0.1", + "purescript-leafletjs": "^0.2.0", + "purescript-halogen-css": "^6.0.0" + }, + "devDependencies": { + "purescript-debug": "^3.0.0", + "purescript-random": "^3.0.0" + } +} diff --git a/example/entry.js b/example/entry.js new file mode 100644 index 0000000..90c5896 --- /dev/null +++ b/example/entry.js @@ -0,0 +1 @@ +require("./../output/Main/index.js").main(); diff --git a/example/index.html b/example/index.html new file mode 100644 index 0000000..31a067a --- /dev/null +++ b/example/index.html @@ -0,0 +1,15 @@ + + + + + purescript-halogen-leaflet + + + + + + + + diff --git a/example/leaflet.css b/example/leaflet.css new file mode 100644 index 0000000..c6d920a --- /dev/null +++ b/example/leaflet.css @@ -0,0 +1,624 @@ +/* required styles */ + +.leaflet-pane, +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-tile-container, +.leaflet-pane > svg, +.leaflet-pane > canvas, +.leaflet-zoom-box, +.leaflet-image-layer, +.leaflet-layer { + position: absolute; + left: 0; + top: 0; + } +.leaflet-container { + overflow: hidden; + } +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + -webkit-user-drag: none; + } +/* Safari renders non-retina tile on retina better with this, but Chrome is worse */ +.leaflet-safari .leaflet-tile { + image-rendering: -webkit-optimize-contrast; + } +/* hack that prevents hw layers "stretching" when loading new tiles */ +.leaflet-safari .leaflet-tile-container { + width: 1600px; + height: 1600px; + -webkit-transform-origin: 0 0; + } +.leaflet-marker-icon, +.leaflet-marker-shadow { + display: block; + } +/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ +/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ +.leaflet-container .leaflet-overlay-pane svg, +.leaflet-container .leaflet-marker-pane img, +.leaflet-container .leaflet-shadow-pane img, +.leaflet-container .leaflet-tile-pane img, +.leaflet-container img.leaflet-image-layer { + max-width: none !important; + } + +.leaflet-container.leaflet-touch-zoom { + -ms-touch-action: pan-x pan-y; + touch-action: pan-x pan-y; + } +.leaflet-container.leaflet-touch-drag { + -ms-touch-action: pinch-zoom; + } +.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { + -ms-touch-action: none; + touch-action: none; +} +.leaflet-tile { + filter: inherit; + visibility: hidden; + } +.leaflet-tile-loaded { + visibility: inherit; + } +.leaflet-zoom-box { + width: 0; + height: 0; + -moz-box-sizing: border-box; + box-sizing: border-box; + z-index: 800; + } +/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ +.leaflet-overlay-pane svg { + -moz-user-select: none; + } + +.leaflet-pane { z-index: 400; } + +.leaflet-tile-pane { z-index: 200; } +.leaflet-overlay-pane { z-index: 400; } +.leaflet-shadow-pane { z-index: 500; } +.leaflet-marker-pane { z-index: 600; } +.leaflet-tooltip-pane { z-index: 650; } +.leaflet-popup-pane { z-index: 700; } + +.leaflet-map-pane canvas { z-index: 100; } +.leaflet-map-pane svg { z-index: 200; } + +.leaflet-vml-shape { + width: 1px; + height: 1px; + } +.lvml { + behavior: url(#default#VML); + display: inline-block; + position: absolute; + } + + +/* control positioning */ + +.leaflet-control { + position: relative; + z-index: 800; + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } +.leaflet-top, +.leaflet-bottom { + position: absolute; + z-index: 1000; + pointer-events: none; + } +.leaflet-top { + top: 0; + } +.leaflet-right { + right: 0; + } +.leaflet-bottom { + bottom: 0; + } +.leaflet-left { + left: 0; + } +.leaflet-control { + float: left; + clear: both; + } +.leaflet-right .leaflet-control { + float: right; + } +.leaflet-top .leaflet-control { + margin-top: 10px; + } +.leaflet-bottom .leaflet-control { + margin-bottom: 10px; + } +.leaflet-left .leaflet-control { + margin-left: 10px; + } +.leaflet-right .leaflet-control { + margin-right: 10px; + } + + +/* zoom and fade animations */ + +.leaflet-fade-anim .leaflet-tile { + will-change: opacity; + } +.leaflet-fade-anim .leaflet-popup { + opacity: 0; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + } +.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { + opacity: 1; + } +.leaflet-zoom-animated { + -webkit-transform-origin: 0 0; + -ms-transform-origin: 0 0; + transform-origin: 0 0; + } +.leaflet-zoom-anim .leaflet-zoom-animated { + will-change: transform; + } +.leaflet-zoom-anim .leaflet-zoom-animated { + -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); + -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); + -o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1); + transition: transform 0.25s cubic-bezier(0,0,0.25,1); + } +.leaflet-zoom-anim .leaflet-tile, +.leaflet-pan-anim .leaflet-tile { + -webkit-transition: none; + -moz-transition: none; + -o-transition: none; + transition: none; + } + +.leaflet-zoom-anim .leaflet-zoom-hide { + visibility: hidden; + } + + +/* cursors */ + +.leaflet-interactive { + cursor: pointer; + } +.leaflet-grab { + cursor: -webkit-grab; + cursor: -moz-grab; + } +.leaflet-crosshair, +.leaflet-crosshair .leaflet-interactive { + cursor: crosshair; + } +.leaflet-popup-pane, +.leaflet-control { + cursor: auto; + } +.leaflet-dragging .leaflet-grab, +.leaflet-dragging .leaflet-grab .leaflet-interactive, +.leaflet-dragging .leaflet-marker-draggable { + cursor: move; + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + } + +/* marker & overlays interactivity */ +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-image-layer, +.leaflet-pane > svg path, +.leaflet-tile-container { + pointer-events: none; + } + +.leaflet-marker-icon.leaflet-interactive, +.leaflet-image-layer.leaflet-interactive, +.leaflet-pane > svg path.leaflet-interactive { + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } + +/* visual tweaks */ + +.leaflet-container { + background: #ddd; + outline: 0; + } +.leaflet-container a { + color: #0078A8; + } +.leaflet-container a.leaflet-active { + outline: 2px solid orange; + } +.leaflet-zoom-box { + border: 2px dotted #38f; + background: rgba(255,255,255,0.5); + } + + +/* general typography */ +.leaflet-container { + font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; + } + + +/* general toolbar styles */ + +.leaflet-bar { + box-shadow: 0 1px 5px rgba(0,0,0,0.65); + border-radius: 4px; + } +.leaflet-bar a, +.leaflet-bar a:hover { + background-color: #fff; + border-bottom: 1px solid #ccc; + width: 26px; + height: 26px; + line-height: 26px; + display: block; + text-align: center; + text-decoration: none; + color: black; + } +.leaflet-bar a, +.leaflet-control-layers-toggle { + background-position: 50% 50%; + background-repeat: no-repeat; + display: block; + } +.leaflet-bar a:hover { + background-color: #f4f4f4; + } +.leaflet-bar a:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + } +.leaflet-bar a:last-child { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom: none; + } +.leaflet-bar a.leaflet-disabled { + cursor: default; + background-color: #f4f4f4; + color: #bbb; + } + +.leaflet-touch .leaflet-bar a { + width: 30px; + height: 30px; + line-height: 30px; + } + + +/* zoom control */ + +.leaflet-control-zoom-in, +.leaflet-control-zoom-out { + font: bold 18px 'Lucida Console', Monaco, monospace; + text-indent: 1px; + } +.leaflet-control-zoom-out { + font-size: 20px; + } + +.leaflet-touch .leaflet-control-zoom-in { + font-size: 22px; + } +.leaflet-touch .leaflet-control-zoom-out { + font-size: 24px; + } + + +/* layers control */ + +.leaflet-control-layers { + box-shadow: 0 1px 5px rgba(0,0,0,0.4); + background: #fff; + border-radius: 5px; + } +.leaflet-control-layers-toggle { + background-image: url(images/layers.png); + width: 36px; + height: 36px; + } +.leaflet-retina .leaflet-control-layers-toggle { + background-image: url(images/layers-2x.png); + background-size: 26px 26px; + } +.leaflet-touch .leaflet-control-layers-toggle { + width: 44px; + height: 44px; + } +.leaflet-control-layers .leaflet-control-layers-list, +.leaflet-control-layers-expanded .leaflet-control-layers-toggle { + display: none; + } +.leaflet-control-layers-expanded .leaflet-control-layers-list { + display: block; + position: relative; + } +.leaflet-control-layers-expanded { + padding: 6px 10px 6px 6px; + color: #333; + background: #fff; + } +.leaflet-control-layers-scrollbar { + overflow-y: scroll; + padding-right: 5px; + } +.leaflet-control-layers-selector { + margin-top: 2px; + position: relative; + top: 1px; + } +.leaflet-control-layers label { + display: block; + } +.leaflet-control-layers-separator { + height: 0; + border-top: 1px solid #ddd; + margin: 5px -10px 5px -6px; + } + +/* Default icon URLs */ +.leaflet-default-icon-path { + background-image: url(images/marker-icon.png); + } + + +/* attribution and scale controls */ + +.leaflet-container .leaflet-control-attribution { + background: #fff; + background: rgba(255, 255, 255, 0.7); + margin: 0; + } +.leaflet-control-attribution, +.leaflet-control-scale-line { + padding: 0 5px; + color: #333; + } +.leaflet-control-attribution a { + text-decoration: none; + } +.leaflet-control-attribution a:hover { + text-decoration: underline; + } +.leaflet-container .leaflet-control-attribution, +.leaflet-container .leaflet-control-scale { + font-size: 11px; + } +.leaflet-left .leaflet-control-scale { + margin-left: 5px; + } +.leaflet-bottom .leaflet-control-scale { + margin-bottom: 5px; + } +.leaflet-control-scale-line { + border: 2px solid #777; + border-top: none; + line-height: 1.1; + padding: 2px 5px 1px; + font-size: 11px; + white-space: nowrap; + overflow: hidden; + -moz-box-sizing: border-box; + box-sizing: border-box; + + background: #fff; + background: rgba(255, 255, 255, 0.5); + } +.leaflet-control-scale-line:not(:first-child) { + border-top: 2px solid #777; + border-bottom: none; + margin-top: -2px; + } +.leaflet-control-scale-line:not(:first-child):not(:last-child) { + border-bottom: 2px solid #777; + } + +.leaflet-touch .leaflet-control-attribution, +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + box-shadow: none; + } +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + border: 2px solid rgba(0,0,0,0.2); + background-clip: padding-box; + } + + +/* popup */ + +.leaflet-popup { + position: absolute; + text-align: center; + margin-bottom: 20px; + } +.leaflet-popup-content-wrapper { + padding: 1px; + text-align: left; + border-radius: 12px; + } +.leaflet-popup-content { + margin: 13px 19px; + line-height: 1.4; + } +.leaflet-popup-content p { + margin: 18px 0; + } +.leaflet-popup-tip-container { + width: 40px; + height: 20px; + position: absolute; + left: 50%; + margin-left: -20px; + overflow: hidden; + pointer-events: none; + } +.leaflet-popup-tip { + width: 17px; + height: 17px; + padding: 1px; + + margin: -10px auto 0; + + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + -o-transform: rotate(45deg); + transform: rotate(45deg); + } +.leaflet-popup-content-wrapper, +.leaflet-popup-tip { + background: white; + color: #333; + box-shadow: 0 3px 14px rgba(0,0,0,0.4); + } +.leaflet-container a.leaflet-popup-close-button { + position: absolute; + top: 0; + right: 0; + padding: 4px 4px 0 0; + border: none; + text-align: center; + width: 18px; + height: 14px; + font: 16px/14px Tahoma, Verdana, sans-serif; + color: #c3c3c3; + text-decoration: none; + font-weight: bold; + background: transparent; + } +.leaflet-container a.leaflet-popup-close-button:hover { + color: #999; + } +.leaflet-popup-scrolled { + overflow: auto; + border-bottom: 1px solid #ddd; + border-top: 1px solid #ddd; + } + +.leaflet-oldie .leaflet-popup-content-wrapper { + zoom: 1; + } +.leaflet-oldie .leaflet-popup-tip { + width: 24px; + margin: 0 auto; + + -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; + filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); + } +.leaflet-oldie .leaflet-popup-tip-container { + margin-top: -1px; + } + +.leaflet-oldie .leaflet-control-zoom, +.leaflet-oldie .leaflet-control-layers, +.leaflet-oldie .leaflet-popup-content-wrapper, +.leaflet-oldie .leaflet-popup-tip { + border: 1px solid #999; + } + + +/* div icon */ + +.leaflet-div-icon { + background: #fff; + border: 1px solid #666; + } + + +/* Tooltip */ +/* Base styles for the element that has a tooltip */ +.leaflet-tooltip { + position: absolute; + padding: 6px; + background-color: #fff; + border: 1px solid #fff; + border-radius: 3px; + color: #222; + white-space: nowrap; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + box-shadow: 0 1px 3px rgba(0,0,0,0.4); + } +.leaflet-tooltip.leaflet-clickable { + cursor: pointer; + pointer-events: auto; + } +.leaflet-tooltip-top:before, +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + position: absolute; + pointer-events: none; + border: 6px solid transparent; + background: transparent; + content: ""; + } + +/* Directions */ + +.leaflet-tooltip-bottom { + margin-top: 6px; +} +.leaflet-tooltip-top { + margin-top: -6px; +} +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-top:before { + left: 50%; + margin-left: -6px; + } +.leaflet-tooltip-top:before { + bottom: 0; + margin-bottom: -12px; + border-top-color: #fff; + } +.leaflet-tooltip-bottom:before { + top: 0; + margin-top: -12px; + margin-left: -6px; + border-bottom-color: #fff; + } +.leaflet-tooltip-left { + margin-left: -6px; +} +.leaflet-tooltip-right { + margin-left: 6px; +} +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + top: 50%; + margin-top: -6px; + } +.leaflet-tooltip-left:before { + right: 0; + margin-right: -12px; + border-left-color: #fff; + } +.leaflet-tooltip-right:before { + left: 0; + margin-left: -12px; + border-right-color: #fff; + } diff --git a/example/marker.svg b/example/marker.svg new file mode 100644 index 0000000..ba1af69 --- /dev/null +++ b/example/marker.svg @@ -0,0 +1,19 @@ + + + + + + + diff --git a/example/src/Main.purs b/example/src/Main.purs new file mode 100644 index 0000000..6474841 --- /dev/null +++ b/example/src/Main.purs @@ -0,0 +1,160 @@ +module Main where + +import Prelude + +import Control.MonadPlus (class MonadPlus) +import Control.Monad.Aff (Aff) +import Control.Monad.Aff.Class (class MonadAff, liftAff) +import Control.Monad.Eff (Eff) +import Control.Monad.Eff.Class (liftEff) +import Control.Monad.Eff.Random (RANDOM, random) +import Control.Monad.Rec.Class (class MonadRec) + +import Data.Array as A +import Data.Either (Either(..)) +import Data.Traversable as F +import Data.Maybe (Maybe(..), isNothing) +import Data.Newtype (under) +import Data.Path.Pathy ((), (<.>), file, currentDir, rootDir, dir) +import Data.Profunctor (lmap) +import Data.URI as URI +import Data.URI (URIRef) + +import Graphics.Canvas (CANVAS) + +import Halogen.Aff as HA +import Halogen.VDom.Driver (runUI) +import Halogen.Component as HC +import Halogen.Component.Profunctor as HPR +import Halogen as H +import Halogen.HTML as HH +import Halogen.HTML.Events as HE + +import Leaflet.Halogen as HL +import Leaflet.Core as LC +import Leaflet.Plugin.Heatmap as LH +import Leaflet.Util ((×)) + +data Query a + = HandleMessage Slot HL.Message a + | SetWidth a + | AddMarker a + | RemoveMarker a + +type State = + { marker ∷ Maybe LC.Marker + , firstSize ∷ { width ∷ Int, height ∷ Int } + , secondSize ∷ { width ∷ Int, height ∷ Int } + } + +type Input = Unit + +type Slot = Int + +type Effects = HA.HalogenEffects (HL.Effects (canvas ∷ CANVAS, random ∷ RANDOM)) +type MainAff = Aff Effects +type HTML = H.ParentHTML Query HL.Query Slot MainAff +type DSL = H.ParentDSL State Query HL.Query Slot Void MainAff + +initialState ∷ Input → State +initialState _ = + { marker: Nothing + , firstSize: { width: 400, height: 600 } + , secondSize: { width: 400, height: 600 } + } + +ui ∷ H.Component HH.HTML Query Unit Void MainAff +ui = H.parentComponent + { initialState + , render + , eval + , receiver: const Nothing + } + where + leaflet = + HC.unComponent (\cfg → + HC.mkComponent cfg{ receiver = \{width, height} → + Just $ H.action $ HL.SetDimension { width: Just width, height: Just height } } ) + $ under HPR.ProComponent (lmap $ const unit) HL.leaflet + + render ∷ State → HTML + render state = + HH.div_ + [ HH.slot 0 leaflet state.firstSize (HE.input $ HandleMessage 0) + , HH.button [ HE.onClick (HE.input_ SetWidth) ][ HH.text "resize me" ] + , HH.button [ HE.onClick (HE.input_ AddMarker) ] [ HH.text "add marker" ] + , HH.button [ HE.onClick (HE.input_ RemoveMarker) ] [ HH.text "remove marker" ] + , HH.slot 1 leaflet state.secondSize (HE.input $ HandleMessage 1) + ] + + eval ∷ Query ~> DSL + eval = case _ of + HandleMessage 0 (HL.Initialized _) next → do + tiles ← LC.tileLayer osmURI + void $ H.query 0 $ H.action $ HL.AddLayers [ LC.tileToLayer tiles ] + pure next + HandleMessage _ (HL.Initialized leaf) next → do + tiles ← LC.tileLayer osmURI + heatmap ← LC.layer + heatmapData ← liftAff mkHeatmapData + layState ← LH.mkHeatmap LH.defaultOptions heatmapData heatmap leaf + void $ H.query 1 $ H.action $ HL.AddLayers [ LC.tileToLayer tiles, heatmap ] + pure next + SetWidth next → do + H.modify _{ firstSize = { height: 200, width: 1000 } } + pure next + AddMarker next → do + state ← H.get + when (isNothing state.marker) do + latLng ← liftAff $ LC.mkLatLng (-37.87) 175.457 + icon ← LC.icon iconConf + marker ← LC.marker latLng >>= LC.setIcon icon + H.modify _{ marker = Just marker } + void $ H.query 0 $ H.action $ HL.AddLayers [ LC.markerToLayer marker ] + pure next + RemoveMarker next → do + state ← H.get + F.for_ state.marker \marker → do + void $ H.query 0 $ H.action $ HL.RemoveLayers [ LC.markerToLayer marker ] + H.modify _{ marker = Nothing } + pure next + + iconConf ∷ { iconUrl ∷ URIRef, iconSize ∷ LC.Point } + iconConf = + { iconUrl: Right $ URI.RelativeRef + (URI.RelativePart Nothing $ Just $ Right $ currentDir file "marker" <.> "svg") + Nothing + Nothing + , iconSize: 40 × 40 + } + + osmURI ∷ URIRef + osmURI = + Left $ URI.URI + (Just $ URI.URIScheme "http") + (URI.HierarchicalPart + (Just $ URI.Authority Nothing [(URI.NameAddress "{s}.tile.osm.org") × Nothing]) + (Just $ Right $ rootDir dir "{z}" dir "{x}" file "{y}" <.> "png")) + Nothing + Nothing + + mkHeatmapData + ∷ ∀ m + . MonadAff Effects m + ⇒ MonadPlus m + ⇒ MonadRec m + ⇒ m (Array { lat ∷ LC.Degrees, lng ∷ LC.Degrees, i ∷ Number }) + mkHeatmapData = do + let + inp = A.range 0 10000 + foldFn acc _ = do + xDiff ← liftEff random + lat ← LC.mkDegrees $ xDiff / 30.0 - 37.87 + yDiff ← liftEff random + lng ← LC.mkDegrees $ yDiff / 40.0 + 175.457 + i ← map (_ / 2.0) $ liftEff random + pure $ A.snoc acc { lat, lng, i } + A.foldRecM foldFn [] inp + +main ∷ Eff Effects Unit +main = HA.runHalogenAff $ runUI ui unit =<< HA.awaitBody diff --git a/package.json b/package.json new file mode 100644 index 0000000..3d43817 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "purescript-leafletjs-halogen", + "scripts": { + "build": "pulp build -- --censor-lib --strict --stash", + "build:non-strict": "pulp build", + "run:test": "pulp test -- --censor-lib --strict --stash", + "run": "pulp run", + "build:browser": "pulp browserify --include example --to example/index.js", + "build:webpack": "webpack", + "build:dev": "webpack --watch", + "ide": "purs ide server" + }, + "dependencies": { + "leaflet": "^1.0.3", + "pulp": "^11.0.0", + "purescript": "^0.11.4", + "purescript-psa": "^0.5.1", + "webpack": "^2.4.1" + } +} diff --git a/src/Leaflet/Halogen.purs b/src/Leaflet/Halogen.purs new file mode 100644 index 0000000..7506b69 --- /dev/null +++ b/src/Leaflet/Halogen.purs @@ -0,0 +1,143 @@ +module Leaflet.Halogen where + +import Prelude + +import Control.Monad.Eff (kind Effect) +import Control.Monad.Aff (delay) +import Control.Monad.Aff.Class (class MonadAff, liftAff) + +import CSS.Geometry (width, height) +import CSS.Size (px, pct) + +import Data.Int as Int +import Data.Maybe (Maybe(..), fromJust) +import Data.Traversable as F +import Data.Time.Duration (Milliseconds(..)) + +import DOM (DOM) + +import Halogen as H +import Halogen.HTML as HH +import Halogen.HTML.CSS (style) +import Halogen.HTML.Properties as HP + +import Leaflet.Core as LC +import Leaflet.Util ((∘)) + +import Partial.Unsafe (unsafePartial) + +type State = + { width ∷ Int + , height ∷ Int + , view ∷ LC.LatLng + , zoom ∷ LC.Zoom + , leaflet ∷ Maybe LC.Leaflet + } + +data Query a + = Init a + | SetDimension { width ∷ Maybe Int, height ∷ Maybe Int } a + -- Note, layers are not preserved in state, parent component must + -- handle it. + | AddLayers (Array LC.Layer) a + | RemoveLayers (Array LC.Layer) a + | GetLeaflet (Maybe LC.Leaflet → a) + | SetView LC.LatLng a + | SetZoom LC.Zoom a + +data Message + = Initialized LC.Leaflet + +type Effects (e ∷ # Effect) = + ( dom ∷ DOM + | e ) + +type HTML = H.ComponentHTML Query + +type DSL m = H.ComponentDSL State Query Message m + +type Input = Unit + +initialState ∷ ∀ i. i → State +initialState i = + { height: 400 + , width: 600 + , view: unsafePartial fromJust $ LC.mkLatLng (-37.87) 175.457 + , zoom: unsafePartial fromJust $ LC.mkZoom 12 + , leaflet: Nothing + } + +leaflet + ∷ ∀ e m + . MonadAff (Effects e) m + ⇒ H.Component HH.HTML Query Input Message m +leaflet = H.lifecycleComponent + { initialState + , render + , eval + , initializer: Just $ H.action Init + , finalizer: Nothing + , receiver: const Nothing + } + +render ∷ State → HTML +render state = + HH.div + [ style do + height $ px $ Int.toNumber state.height + width $ px $ Int.toNumber state.width + ] + [ HH.div + [ HP.ref $ H.RefLabel "leaflet" + , style do + height $ pct 100.0 + width $ pct 100.0 + ] [ ] ] + + +eval ∷ ∀ e m. MonadAff (Effects e) m ⇒ Query ~> DSL m +eval = case _ of + Init next → do + state ← H.get + void $ H.getHTMLElementRef (H.RefLabel "leaflet") + >>= F.traverse \el → do + leaf ← LC.leaflet el + >>= LC.setView state.view + >>= LC.setZoom state.zoom + H.modify _{ leaflet = Just leaf } + H.raise $ Initialized leaf + pure next + SetDimension dim next → do + state ← H.get + F.for_ dim.height \h → + when (h >= 0) $ H.modify _{ height = h } + F.for_ dim.width \w → + when (w >= 0) $ H.modify _{ width = w } + F.for_ state.leaflet \l → do + liftAff $ delay $ Milliseconds 1000.0 + LC.invalidateSize true l + pure next + AddLayers ls next → do + state ← H.get + F.for_ state.leaflet \leaf → + F.for_ ls \layer → + void $ LC.addLayer layer leaf + pure next + RemoveLayers ls next → do + state ← H.get + F.for_ state.leaflet \leaf → + F.for_ ls \layer → + void $ LC.removeLayer layer leaf + pure next + GetLeaflet continue → do + H.gets $ continue ∘ _.leaflet + SetView v next → do + state ← H.get + F.for_ state.leaflet $ LC.setView v + H.modify _{ view = v } + pure next + SetZoom zoom next → do + state ← H.get + F.for_ state.leaflet $ LC.setZoom zoom + H.modify _{ zoom = zoom } + pure next diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..817f8fc --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,6 @@ +module.exports = { + entry: "./example/entry.js", + output: { + filename: "./example/index.js" + } +};