Repository: bokeh/bokeh

Issue:
[BUG] `GeoJSONDataSource` incorrectly processes `MultiPolygon` geometry type
#### ALL software version info (bokeh, python, notebook, OS, browser, any other relevant packages)
Bokeh 2.1.0

#### Complete, minimal, self-contained example code that reproduces the issue
```python
from bokeh.io import show
from bokeh.models import GeoJSONDataSource, GMapOptions
from bokeh.plotting import gmap

data = """{
  "type": "FeatureCollection",
  "features": [
    {
      "id": "0",
      "type": "Feature",
      "properties": {"plotted_data": 98.7805281},
      "geometry": {
        "type": "MultiPolygon",
        "coordinates": [
          [
            [
              [-98.11372330298127, 30.48557591185999],
              [-98.11336081363682, 30.48592948588511],
              [-98.11319281272974, 30.486219485691766]
            ]
          ],
          [
            [
              [-98.10916428343725, 30.492520477204575],
              [-98.10904979183817, 30.49274448101661],
              [-98.10904600029392, 30.492747999424935]
            ]
          ]
        ]
      }
    }
  ]
}
"""
data_source = GeoJSONDataSource(geojson=data)

map_options = GMapOptions(lat=30.2861, lng=-97.7394, map_type="roadmap", zoom=11)

plot = gmap(%YOUR_API_KEY%, map_options)

plot.patches('xs', 'ys', source=data_source,
             line_color='black', line_width=0.25, fill_alpha=1)

show(plot)
```

`GeoJSONDataSource._add_geometry` has this function:
```typescript
    function flatten(acc: Position[], item: Position[]) {
      return acc.concat([[NaN, NaN, NaN]]).concat(item)
    }
```
And it's incompatible with this code in `proj4/lib/checkSanity.js`:
```javascript
        if (typeof Number.isFinite === 'function') {
            if (Number.isFinite(num)) {
                return;
            }
            throw new TypeError('coordinates must be finite numbers');
        }
```

Given that the same `flatten` function is used for the `MultiLineString` geometry type, it might also be broken.

#### Stack traceback and/or browser JavaScript console output
```
Uncaught (in promise) TypeError: coordinates must be finite numbers
    at checkCoord (checkSanity.js:13)
    at Object.default_1 [as default] (checkSanity.js:4)
    at Object.transform [as default] (transform.js:18)
    at transformer (core.js:10)
    at Object.forward (core.js:81)
    at project_xy (projections.js:32)
    at Object.project_xsys (projections.js:44)
    at PatchesView.set_data (glyph.js:187)
    at GlyphRendererView.set_data (glyph_renderer.js:123)
    at GlyphRendererView.lazy_initialize (glyph_renderer.js:68)
```



Discussion:
Was this working before 2.1?
It was working in 1.4 and stopped in 2.0.
The culprit is 83301c115cf9154abac2d48c8995f90927804e52 that has updated the `proj4` dependency. In `<2.4`, it wasn't doing any checks, but it introduced them in 2.4.5-alpha in https://github.com/proj4js/proj4js/commit/42fae04d1257b9689fa372ea674a3a749636d468
An easy fix would be to not call `wgs84_mercator.forward` if both of the coords are `NaN`. But I'm not sure what to do when just one of them is `NaN`. But crashing the whole page is probably not the way to go. Report it via `console.error` - sure. But the end user will never see this error because there's nothing in the UI that says that something's wrong.

Resolving patch:
diff --git a/bokeh/models/tools.py b/bokeh/models/tools.py
--- a/bokeh/models/tools.py
+++ b/bokeh/models/tools.py
@@ -891,7 +891,7 @@ class CustomJSHover(Model):
                 var projections = Bokeh.require("core/util/projections");
                 var x = special_vars.x
                 var y = special_vars.y
-                var coords = projections.wgs84_mercator.inverse([x, y])
+                var coords = projections.wgs84_mercator.invert(x, y)
                 return "" + coords[1]
             """)
 
diff --git a/examples/plotting/file/customjs_hover.py b/examples/plotting/file/customjs_hover.py
--- a/examples/plotting/file/customjs_hover.py
+++ b/examples/plotting/file/customjs_hover.py
@@ -16,7 +16,7 @@
     var projections = Bokeh.require("core/util/projections");
     var x = special_vars.x
     var y = special_vars.y
-    var coords = projections.wgs84_mercator.inverse([x, y])
+    var coords = projections.wgs84_mercator.invert(x, y)
     return coords[%d].toFixed(2)
 """
 


Test patch:
diff --git a/bokehjs/test/unit/core/util/projections.ts b/bokehjs/test/unit/core/util/projections.ts
--- a/bokehjs/test/unit/core/util/projections.ts
+++ b/bokehjs/test/unit/core/util/projections.ts
@@ -1,10 +1,10 @@
 import {expect} from "assertions"
 
-import {clip_mercator, in_bounds} from "@bokehjs/core/util/projections"
+import {clip_mercator, in_bounds, project_xy, project_xsys} from "@bokehjs/core/util/projections"
 
 describe("core/util/projections module", () => {
 
-  describe("mercator_bounds function", () => {
+  describe("mercator_bounds() function", () => {
 
     it("should not clip valid longitudes", () => {
       expect(clip_mercator(-10018754, 10018754, 'lon')).to.be.equal([-10018754, 10018754])
@@ -39,7 +39,7 @@ describe("core/util/projections module", () => {
     })
   })
 
-  describe("in_bounds function", () => {
+  describe("in_bounds() function", () => {
 
     it("should reject value below lon lower bound", () => {
       expect(in_bounds(-181, 'lon')).to.be.false
@@ -65,4 +65,34 @@ describe("core/util/projections module", () => {
       expect(in_bounds(0, 'lat')).to.be.true
     })
   })
+
+  describe("project_xy() function", () => {
+    it("should handle NaNs and infinities", () => {
+      expect(project_xy([NaN], [1])).to.be.equal([new Float64Array([NaN]), new Float64Array([NaN])])
+      expect(project_xy([1], [NaN])).to.be.equal([new Float64Array([NaN]), new Float64Array([NaN])])
+      expect(project_xy([NaN], [NaN])).to.be.equal([new Float64Array([NaN]), new Float64Array([NaN])])
+
+      expect(project_xy([Infinity], [1])).to.be.equal([new Float64Array([NaN]), new Float64Array([NaN])])
+      expect(project_xy([1], [Infinity])).to.be.equal([new Float64Array([NaN]), new Float64Array([NaN])])
+      expect(project_xy([Infinity], [Infinity])).to.be.equal([new Float64Array([NaN]), new Float64Array([NaN])])
+
+      expect(project_xy([-Infinity], [1])).to.be.equal([new Float64Array([NaN]), new Float64Array([NaN])])
+      expect(project_xy([1], [-Infinity])).to.be.equal([new Float64Array([NaN]), new Float64Array([NaN])])
+      expect(project_xy([-Infinity], [-Infinity])).to.be.equal([new Float64Array([NaN]), new Float64Array([NaN])])
+    })
+  })
+
+  describe("project_xsys() function", () => {
+      expect(project_xsys([[NaN]], [[1]])).to.be.equal([[new Float64Array([NaN])], [new Float64Array([NaN])]])
+      expect(project_xsys([[1]], [[NaN]])).to.be.equal([[new Float64Array([NaN])], [new Float64Array([NaN])]])
+      expect(project_xsys([[NaN]], [[NaN]])).to.be.equal([[new Float64Array([NaN])], [new Float64Array([NaN])]])
+
+      expect(project_xsys([[Infinity]], [[1]])).to.be.equal([[new Float64Array([NaN])], [new Float64Array([NaN])]])
+      expect(project_xsys([[1]], [[Infinity]])).to.be.equal([[new Float64Array([NaN])], [new Float64Array([NaN])]])
+      expect(project_xsys([[Infinity]], [[Infinity]])).to.be.equal([[new Float64Array([NaN])], [new Float64Array([NaN])]])
+
+      expect(project_xsys([[-Infinity]], [[1]])).to.be.equal([[new Float64Array([NaN])], [new Float64Array([NaN])]])
+      expect(project_xsys([[1]], [[-Infinity]])).to.be.equal([[new Float64Array([NaN])], [new Float64Array([NaN])]])
+      expect(project_xsys([[-Infinity]], [[-Infinity]])).to.be.equal([[new Float64Array([NaN])], [new Float64Array([NaN])]])
+  })
 })
diff --git a/bokehjs/test/unit/models/formatters/mercator_tick_formatter.ts b/bokehjs/test/unit/models/formatters/mercator_tick_formatter.ts
--- a/bokehjs/test/unit/models/formatters/mercator_tick_formatter.ts
+++ b/bokehjs/test/unit/models/formatters/mercator_tick_formatter.ts
@@ -16,7 +16,7 @@ describe("mercator_tick_formatter module", () => {
     const obj = new MercatorTickFormatter({dimension: 'lat'})
     for (const lat of [-72, -60.5, -30, -2, 1, -0.5, 0, 0.5, 1, 10, 33.7, 42.123, 50]) {
       for (const lon of [-120, -90, -88, -32.7, -10, -1, 0, 0.5, 1, 5, 12.3, 57, 60.123, 95, 110.1, 120, 130]) {
-        const [mlon, mlat] = wgs84_mercator.forward([lon, lat])
+        const [mlon, mlat] = wgs84_mercator.compute(lon, lat)
         const labels = obj.doFormat([mlat], {loc: mlon})
         expect(labels[0]).to.be.equal(`${lat}`)
       }
@@ -27,7 +27,7 @@ describe("mercator_tick_formatter module", () => {
     const obj = new MercatorTickFormatter({dimension: 'lon'})
     for (const lat of [-72, -60.5, -30, -2, 1, -0.5, 0, 0.5, 1, 10, 33.7, 42.123, 50]) {
       for (const lon of [-120, -90, -88, -32.7, -10, -1, 0, 0.5, 1, 5, 12.3, 57, 60.123, 95, 110.1, 120, 130]) {
-        const [mlon, mlat] = wgs84_mercator.forward([lon, lat])
+        const [mlon, mlat] = wgs84_mercator.compute(lon, lat)
         const labels = obj.doFormat([mlon], {loc: mlat})
         expect(labels[0]).to.be.equal(`${lon}`)
       }