diff --git a/docs/en_US/editgrid.rst b/docs/en_US/editgrid.rst
index 45ef597..858b337 100644
--- a/docs/en_US/editgrid.rst
+++ b/docs/en_US/editgrid.rst
@@ -95,6 +95,26 @@ To delete a row, press the *Delete* toolbar button.  A popup will open, asking y
 
 To commit the changes to the server, select the *Save* toolbar button.  Modifications to a row are written to the server automatically when you select a different row.
 
+**Geometry Data Viewer**
+
+If with PostGIS installed, you can view GIS objects in a map by selecting row(s) and clicking the 'View Geometry' button in the table. If no rows are selected, it will load and render all the rows:
+
+.. image:: images/geometry_viewer.png
+    :alt: Geometry Viewer Button
+
+You can adjust the layout by dragging the title of the panel. To view the properties of the geometries directly in map, just click the specific geometry:
+
+.. image:: images/geometry_viewer_property_table.png
+    :alt: Geometry Viewer Property Table
+
+Below are the default strategies for parsing data:
+
+- *Supported data type:* Geometry Viewer support 2d and 3dm geometries in EWKB format including `Point, LineString, Polygon MultiPoint, MultiLineString, MultiPolygon and GeometryCollection`.
+
+- *About SRID:* If there are geometries with different SRIDs in the same column, the viewer will render geometries with the same SRID in the map. If SRID=4326 the OSM tile layer will be added into the map.
+
+- *About data size:* For performance considerations, the viewer will render geometries no more than 100000, totaling up to 20MB.
+
 **Sort/Filter options dialog**
 
 You can access *Sort/Filter options dialog* by clicking on Sort/Filter button. This allows you to specify an SQL Filter to limit the data displayed and data sorting options in the edit grid window:
@@ -120,4 +140,3 @@ To delete a row from the grid, click the trash icon.
 * Click the *Help* button (?) to access online help.
 * Click the *Ok* button to save work.
 * Click the *Close* button to discard current changes and close the dialog.
-
diff --git a/docs/en_US/images/geometry_viewer.png b/docs/en_US/images/geometry_viewer.png
new file mode 100644
index 0000000000000000000000000000000000000000..733b921a7efe21403cc8be85f4de497e39e18d07
GIT binary patch
diff --git a/docs/en_US/images/geometry_viewer_property_table.png b/docs/en_US/images/geometry_viewer_property_table.png
new file mode 100644
index 0000000000000000000000000000000000000000..254186e4f5bc92196c419be40ee4e8cf19ec0f0c
GIT binary patch
diff --git a/libraries.txt b/libraries.txt
index 6875887..b70eb4a 100644
--- a/libraries.txt
+++ b/libraries.txt
@@ -39,3 +39,5 @@ Open Sans           2.0            AL           https://fonts.googleapis.com/css
 Spectrum            1.8            MIT          https://bgrins.github.io/spectrum/
 Mousetrap           1.6.1          AL           https://github.com/ccampbell/mousetrap
 Axios               0.18.0         MIT          https://github.com/axios/axios
+leaflet             1.3.3          BSD          https://leafletjs.com/
+wkx                 0.4.5          MIT          https://github.com/cschwarz/wkx
diff --git a/web/package.json b/web/package.json
index a551af1..1025e28 100644
--- a/web/package.json
+++ b/web/package.json
@@ -74,6 +74,7 @@
     "jquery": "3.3.1",
     "jquery-contextmenu": "^2.6.4",
     "jquery-ui": "^1.12.1",
+    "leaflet": "^1.3.3",
     "moment": "^2.20.1",
     "mousetrap": "^1.6.1",
     "prop-types": "^15.5.10",
@@ -91,7 +92,8 @@
     "underscore": "^1.8.3",
     "underscore.string": "^3.3.4",
     "watchify": "~3.9.0",
-    "webcabin-docker": "git+https://github.com/EnterpriseDB/wcDocker"
+    "webcabin-docker": "git+https://github.com/EnterpriseDB/wcDocker",
+    "wkx": "^0.4.5"
   },
   "scripts": {
     "linter": "yarn eslint --no-eslintrc -c .eslintrc.js --ext .js --ext .jsx .",
diff --git a/web/pgadmin/static/bundle/slickgrid.js b/web/pgadmin/static/bundle/slickgrid.js
index befb67e..09a5373 100644
--- a/web/pgadmin/static/bundle/slickgrid.js
+++ b/web/pgadmin/static/bundle/slickgrid.js
@@ -8,5 +8,7 @@ import 'slickgrid/slick.formatters';
 import 'slickgrid/plugins/slick.autotooltips';
 import 'slickgrid/plugins/slick.cellrangedecorator';
 import 'slickgrid/plugins/slick.cellrangeselector';
+import 'sources/slickgrid/custom_header_buttons';
+
+export default window.Slick;
 
-export default window.Slick;
\ No newline at end of file
diff --git a/web/pgadmin/static/css/style.css b/web/pgadmin/static/css/style.css
index ed88eab..d9d39bf 100644
--- a/web/pgadmin/static/css/style.css
+++ b/web/pgadmin/static/css/style.css
@@ -14,6 +14,7 @@
 @import '~webcabin-docker/Build/wcDocker.css';
 @import '~acitree/css/aciTree.css';
 @import '~spectrum-colorpicker/spectrum.css';
+@import '~leaflet/dist/leaflet.css';
 
 @import '~codemirror/lib/codemirror.css';
 @import '~codemirror/addon/dialog/dialog.css';
diff --git a/web/pgadmin/static/js/slickgrid/custom_header_buttons.js b/web/pgadmin/static/js/slickgrid/custom_header_buttons.js
new file mode 100644
index 0000000..90bef69
--- /dev/null
+++ b/web/pgadmin/static/js/slickgrid/custom_header_buttons.js
@@ -0,0 +1,180 @@
+(function ($) {
+  // register namespace
+  $.extend(true, window, {
+    'Slick': {
+      'Plugins': {
+        'HeaderButtons': HeaderButtons,
+      },
+    },
+  });
+
+
+  /***
+   * custom header button modified from slick.headerbuttons.js
+   *
+   * USAGE:
+   *
+   * Add the plugin .js & .css files and register it with the grid.
+   *
+   * To specify a custom button in a column header, extend the column definition like so:
+   *
+   *   var columns = [
+   *     {
+   *       id: 'myColumn',
+   *       name: 'My column',
+   *
+   *       // This is the relevant part
+   *       header: {
+   *          buttons: [
+   *              {
+   *                // button options
+   *              },
+   *              {
+   *                // button options
+   *              }
+   *          ]
+   *       }
+   *     }
+   *   ];
+   *
+   * Available button options:
+   *    cssClass:     CSS class to add to the button.
+   *    image:        Relative button image path.
+   *    tooltip:      Button tooltip.
+   *    showOnHover:  Only show the button on hover.
+   *    handler:      Button click handler.
+   *    command:      A command identifier to be passed to the onCommand event handlers.
+   *
+   * The plugin exposes the following events:
+   *    onCommand:    Fired on button click for buttons with 'command' specified.
+   *        Event args:
+   *            grid:     Reference to the grid.
+   *            column:   Column definition.
+   *            command:  Button command identified.
+   *            button:   Button options.  Note that you can change the button options in your
+   *                      event handler, and the column header will be automatically updated to
+   *                      reflect them.  This is useful if you want to implement something like a
+   *                      toggle button.
+   *
+   *
+   * @param options {Object} Options:
+   *    buttonCssClass:   a CSS class to use for buttons (default 'slick-header-button')
+   * @class Slick.Plugins.HeaderButtons
+   * @constructor
+   */
+  function HeaderButtons(options) {
+    var _grid;
+    var _self = this;
+    var _handler = new window.Slick.EventHandler();
+    var _defaults = {
+      buttonCssClass: 'slick-header-button',
+    };
+
+
+    function init(grid) {
+      options = $.extend(true, {}, _defaults, options);
+      _grid = grid;
+      _handler
+        .subscribe(_grid.onHeaderCellRendered, handleHeaderCellRendered)
+        .subscribe(_grid.onBeforeHeaderCellDestroy, handleBeforeHeaderCellDestroy);
+
+      // Force the grid to re-render the header now that the events are hooked up.
+      _grid.setColumns(_grid.getColumns());
+    }
+
+
+    function destroy() {
+      _handler.unsubscribeAll();
+    }
+
+
+    function handleHeaderCellRendered(e, args) {
+      var column = args.column;
+
+      if (column.header && column.header.buttons) {
+        // Append buttons in reverse order since they are floated to the right.
+        var i = column.header.buttons.length;
+        while (i--) {
+          var button = column.header.buttons[i];
+          var btn = $('<div></div>')
+            .addClass(options.buttonCssClass)
+            .data('column', column)
+            .data('button', button);
+
+          if (button.content){
+            btn.append(button.content);
+          }
+
+          if (button.showOnHover) {
+            btn.addClass('slick-header-button-hidden');
+          }
+
+          if (button.image) {
+            btn.css('backgroundImage', 'url(' + button.image + ')');
+          }
+
+          if (button.cssClass) {
+            btn.addClass(button.cssClass);
+          }
+
+          if (button.tooltip) {
+            btn.attr('title', button.tooltip);
+          }
+
+          if (button.command) {
+            btn.data('command', button.command);
+          }
+
+          if (button.handler) {
+            btn.bind('click', button.handler);
+          }
+
+          btn
+            .bind('click', handleButtonClick)
+            .prependTo(args.node);
+        }
+      }
+    }
+
+
+    function handleBeforeHeaderCellDestroy(e, args) {
+      var column = args.column;
+
+      if (column.header && column.header.buttons) {
+        // Removing buttons via jQuery will also clean up any event handlers and data.
+        // NOTE: If you attach event handlers directly or using a different framework,
+        //       you must also clean them up here to avoid memory leaks.
+        $(args.node).find('.' + options.buttonCssClass).remove();
+      }
+    }
+
+
+    function handleButtonClick(e) {
+      var command = $(this).data('command');
+      var columnDef = $(this).data('column');
+      var button = $(this).data('button');
+
+      if (command != null) {
+        _self.onCommand.notify({
+          'grid': _grid,
+          'column': columnDef,
+          'command': command,
+          'button': button,
+        }, e, _self);
+
+        // Update the header in case the user updated the button definition in the handler.
+        _grid.updateColumnHeader(columnDef.id);
+      }
+
+      // Stop propagation so that it doesn't register as a header click event.
+      e.preventDefault();
+      e.stopPropagation();
+    }
+
+    $.extend(this, {
+      'init': init,
+      'destroy': destroy,
+      'onCommand': new window.Slick.Event(),
+    });
+  }
+})(window.jQuery);
diff --git a/web/pgadmin/static/js/sqleditor/geometry_viewer.js b/web/pgadmin/static/js/sqleditor/geometry_viewer.js
new file mode 100644
index 0000000..562696e
--- /dev/null
+++ b/web/pgadmin/static/js/sqleditor/geometry_viewer.js
@@ -0,0 +1,418 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import gettext from 'sources/gettext';
+import {Geometry} from 'wkx';
+import {Buffer} from 'buffer';
+import L from 'leaflet';
+import $ from 'jquery';
+
+let GeometryViewer = {
+  panel_closed: true,
+
+  render_geometries: function (handler, items, columns, columnIndex) {
+    let self = this;
+    if (!self.map_component) {
+      self.map_component = initMapComponent();
+    }
+
+    if (self.panel_closed) {
+      let wcDocker = window.wcDocker;
+      let geometry_viewer_panel = handler.gridView.geometry_viewer =
+        handler.gridView.docker.addPanel('geometry_viewer',
+          wcDocker.DOCK.STACKED, handler.gridView.data_output_panel);
+      $('#geometry_viewer_panel')[0].appendChild(self.map_component.mapContainer.get(0));
+      self.panel_closed = false;
+
+      geometry_viewer_panel.on(wcDocker.EVENT.CLOSED, function () {
+        $('#geometry_viewer_panel').empty();
+        self.map_component.clearMap();
+        self.panel_closed = true;
+      });
+
+      geometry_viewer_panel.on(wcDocker.EVENT.RESIZE_ENDED, function () {
+        if (geometry_viewer_panel.isVisible()) {
+          self.map_component.resizeMap();
+        }
+      });
+
+      geometry_viewer_panel.on(wcDocker.EVENT.VISIBILITY_CHANGED, function (visible) {
+        if (visible) {
+          self.map_component.resizeMap();
+        }
+      });
+    }
+
+    handler.gridView.geometry_viewer.focus();
+    self.map_component.clearMap();
+    let dataObj = parseData(items, columns, columnIndex);
+    self.map_component.renderMap(dataObj);
+  },
+
+  add_header_button: function (columnDefinition) {
+    columnDefinition.header = {
+      buttons: [
+        {
+          cssClass: 'div-view-geometry-column',
+          tooltip: 'View all geometries in this column',
+          showOnHover: false,
+          command: 'view-geometries',
+          content: '<button class="btn-xs btn-primary"><i class="fa fa-eye" aria-hidden="true"></i></button>',
+        },
+      ],
+    };
+  },
+
+  parse_data: parseData,
+};
+
+function initMapComponent() {
+  const geojsonMarkerOptions = {
+    radius: 4,
+    weight: 3,
+  };
+  const geojsonStyle = {
+    weight: 2,
+  };
+  const popupOption = {
+    closeButton: false,
+    minWidth: 260,
+    maxWidth: 300,
+    maxHeight: 300,
+  };
+
+  let mapContainer = $('<div class="geometry-viewer-container"></div>');
+  let lmap = L.map(mapContainer.get(0), {
+    preferCanvas: true,
+  }).setZoom(0);
+
+  // update default attribution
+  lmap.attributionControl.setPrefix('<a href="http://leafletjs.com" target="_blank" title="A JS library for interactive maps">Leaflet</a>');
+
+  let vectorLayer = L.geoJSON([], {
+    style: geojsonStyle,
+    pointToLayer: function (feature, latlng) {
+      return L.circleMarker(latlng, geojsonMarkerOptions);
+    },
+  });
+  vectorLayer.addTo(lmap);
+  let baseLayersObj = {
+    'Empty': L.tileLayer(''),
+    'Street': L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
+      {
+        maxZoom: 19,
+        attribution: '&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>',
+      }),
+    'Topography': L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
+      {
+        maxZoom: 17,
+        attribution: 'Map data: &copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>,' +
+          ' <a href="http://viewfinderpanoramas.org" target="_blank">SRTM</a>' +
+          ' | Map style: &copy; <a href="https://opentopomap.org" target="_blank">OpenTopoMap</a>' +
+          ' (<a href="https://creativecommons.org/licenses/by-sa/3.0/" target="_blank">CC-BY-SA</a>)',
+      }),
+    'Gray Style': L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}{r}.png',
+      {
+        attribution: '&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>' +
+          ' &copy; <a href="http://cartodb.com/attributions" target="_blank">CartoDB</a>',
+        subdomains: 'abcd',
+        maxZoom: 19,
+      }),
+    'Light Color': L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/rastertiles/voyager/{z}/{x}/{y}{r}.png',
+      {
+        attribution: '&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>' +
+          ' &copy; <a href="http://cartodb.com/attributions" target="_blank">CartoDB</a>',
+        subdomains: 'abcd',
+        maxZoom: 19,
+      }),
+    'Dark Matter': L.tileLayer('https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}{r}.png',
+      {
+        attribution: '&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>' +
+          ' &copy; <a href="http://cartodb.com/attributions" target="_blank">CartoDB</a>',
+        subdomains: 'abcd',
+        maxZoom: 19,
+      }),
+  };
+  let layerControl = L.control.layers(baseLayersObj);
+  let defaultBaseLayer = baseLayersObj.Street;
+  let baseLayers = _.values(baseLayersObj);
+
+  let infoControl = L.control({position: 'topright'});
+  infoControl.onAdd = function () {
+    this._div = L.DomUtil.create('div', 'geometry-viewer-info-control');
+    return this._div;
+  };
+  infoControl.update = function (content) {
+    this._div.innerHTML = content;
+  };
+
+  let setEPSG3857 = function () {
+    if (lmap.options.crs !== L.CRS.EPSG3857) {
+      lmap.options.crs = L.CRS.EPSG3857;
+      layerControl.addTo(lmap);
+      lmap.addLayer(defaultBaseLayer);
+      mapContainer.addClass('geometry-viewer-container-plain-background');
+      lmap.setMinZoom(0);
+    }
+  };
+
+  let setSimpleCRS = function () {
+    if (lmap.options.crs !== L.CRS.Simple) {
+      lmap.options.crs = L.CRS.Simple;
+      layerControl.remove();
+      _.each(baseLayers, function (layer) {
+        if (lmap.hasLayer(layer)) {
+          defaultBaseLayer = layer;
+          layer.remove();
+        }
+      });
+      mapContainer.removeClass('geometry-viewer-container-plain-background');
+    }
+  };
+
+  setSimpleCRS();
+  setEPSG3857();
+
+  return {
+    'mapContainer': mapContainer,
+    'clearMap': function () {
+      lmap.closePopup();
+      infoControl.remove();
+      vectorLayer.clearLayers();
+    },
+
+    'renderMap': function (dataObj) {
+      let geoJSONs = dataObj.geoJSONs,
+        SRID = dataObj.selectedSRID,
+        getPopupContent = dataObj.getPopupContent,
+        infoList = dataObj.infoList;
+
+      let isEmpty = false;
+      if (geoJSONs.length === 0) {
+        isEmpty = true;
+      }
+
+      try {
+        vectorLayer.addData(geoJSONs);
+      } catch (e) {
+        // Invalid LatLng object: (NaN, NaN)
+        infoList.push('An error occurred while rendering data.');
+        isEmpty = true;
+      }
+
+      let bounds = vectorLayer.getBounds();
+      if (!bounds.isValid()) {
+        isEmpty = true;
+      }
+
+      if (infoList.length > 0) {
+        if (lmap.options.crs === L.CRS.EPSG3857) {
+          layerControl.remove();
+          infoControl.addTo(lmap);
+          layerControl.addTo(lmap);
+        } else {
+          infoControl.addTo(lmap);
+        }
+        let infoContent = generateInfoContent(infoList);
+        infoControl.update(infoContent);
+      }
+
+      if (isEmpty) {
+        setSimpleCRS();
+        lmap.setView([0, 0], lmap.getZoom());
+        return;
+      }
+
+      if (typeof getPopupContent === 'function') {
+        let addPopup = function (layer) {
+          layer.bindPopup(function () {
+            return getPopupContent(layer.feature.geometry);
+          }, popupOption);
+        };
+        vectorLayer.eachLayer(addPopup);
+      }
+
+      bounds = bounds.pad(0.1);
+      let maxLength = Math.max(bounds.getNorth() - bounds.getSouth(),
+        bounds.getEast() - bounds.getWest());
+      if (SRID === 4326) {
+        setEPSG3857();
+      } else {
+        setSimpleCRS();
+        if (maxLength >= 180) {
+          // calculate the min zoom level to enable the map to fit the whole geometry.
+          let minZoom = Math.floor(Math.log2(360 / maxLength)) - 2;
+          lmap.setMinZoom(minZoom);
+        } else {
+          lmap.setMinZoom(0);
+        }
+      }
+
+      if (maxLength > 0) {
+        lmap.fitBounds(bounds);
+      } else {
+        lmap.setView(bounds.getCenter(), lmap.getZoom());
+      }
+    },
+
+    'resizeMap': function () {
+      setTimeout(function () {
+        lmap.invalidateSize();
+      }, 10);
+    },
+
+  };
+}
+
+function parseData(items, columns, columnIndex) {
+  const maxRenderByteLength = 20 * 1024 * 1024; //render geometry data up to 20MB
+  const maxRenderGeometries = 100000; // render geometries up to 100000
+  let field = columns[columnIndex].field;
+  let geometries3D = [],
+    supportedGeometries = [],
+    unsupportedItems = [],
+    infoList = [],
+    geometryItemMap = new Map(),
+    mixedSRID = false,
+    geometryTotalByteLength = 0,
+    tooLargeDataSize = false,
+    tooManyGeometries = false;
+
+  if (items.length === 0) {
+    infoList.push('Empty row.');
+    return {
+      'geoJSONs': [],
+      'selectedSRID': 0,
+      'getPopupContent': undefined,
+      'infoList': infoList,
+    };
+  }
+
+  // parse ewkb data
+  _.every(items, function (item) {
+    try {
+      let value = item[field];
+      let buffer = Buffer.from(value, 'hex');
+      let geometry = Geometry.parse(buffer);
+      if (geometry.hasZ) {
+        geometries3D.push(geometry);
+      } else {
+        geometryTotalByteLength += buffer.byteLength;
+        if (geometryTotalByteLength > maxRenderByteLength) {
+          tooLargeDataSize = true;
+          return false;
+        }
+        if (supportedGeometries.length >= maxRenderGeometries) {
+          tooManyGeometries = true;
+          return false;
+        }
+
+        if (!geometry.srid) {
+          geometry.srid = 0;
+        }
+        supportedGeometries.push(geometry);
+        geometryItemMap.set(geometry, item);
+      }
+    } catch (e) {
+      unsupportedItems.push(item);
+    }
+    return true;
+  });
+
+  // generate map info content
+  if (tooLargeDataSize || tooManyGeometries) {
+    infoList.push(supportedGeometries.length + ' of ' + items.length + ' geometries rendered.');
+  }
+  if (geometries3D.length > 0) {
+    infoList.push(gettext('3D geometries not rendered.'));
+  }
+  if (unsupportedItems.length > 0) {
+    infoList.push(gettext('Unsupported geometries not rendered.'));
+  }
+
+  if (supportedGeometries.length === 0) {
+    return {
+      'geoJSONs': [],
+      'selectedSRID': 0,
+      'getPopupContent': undefined,
+      'infoList': infoList,
+    };
+  }
+
+  // group geometries by SRID
+  let geometriesGroupBySRID = _.groupBy(supportedGeometries, 'srid');
+  let SRIDGeometriesPairs = _.pairs(geometriesGroupBySRID);
+  if (SRIDGeometriesPairs.length > 1) {
+    mixedSRID = true;
+  }
+  // select the largest group
+  let selectedPair = _.max(SRIDGeometriesPairs, function (pair) {
+    return pair[1].length;
+  });
+  let selectedSRID = parseInt(selectedPair[0]);
+  let selectedGeometries = selectedPair[1];
+
+  let geoJSONs = _.map(selectedGeometries, function (geometry) {
+    return geometry.toGeoJSON();
+  });
+
+  let getPopupContent;
+  if (columns.length >= 3) {
+    // add popup when geometry has properties
+    getPopupContent = function (geojson) {
+      let geometry = selectedGeometries[geoJSONs.indexOf(geojson)];
+      let item = geometryItemMap.get(geometry);
+      return itemToTable(item, columns, columnIndex);
+    };
+  }
+
+  if (mixedSRID) {
+    infoList.push(gettext('Geometries with non-SRID') + selectedSRID + ' not rendered.');
+  }
+
+  return {
+    'geoJSONs': geoJSONs,
+    'selectedSRID': selectedSRID,
+    'getPopupContent': getPopupContent,
+    'infoList': infoList,
+  };
+}
+
+function itemToTable(item, columns, ignoredColumnIndex) {
+  let content = '<table class="table table-bordered table-striped view-geometry-property-table"><tbody>';
+
+  // start from 1 because columns[0] is empty
+  for (let i = 1; i < columns.length; i++) {
+    if (i !== ignoredColumnIndex) {
+      let columnDef = columns[i];
+      content += '<tr><th>' + columnDef.display_name + '</th>';
+
+      let value = item[columnDef.field];
+      if (_.isUndefined(value) && columnDef.has_default_val) {
+        content += '<td class="td-disabled">[default]</td>';
+      } else if ((_.isUndefined(value) && columnDef.not_null) ||
+        (_.isUndefined(value) || value === null)) {
+        content += '<td class="td-disabled">[null]</td>';
+      } else {
+        content += '<td>' + value + '</td>';
+      }
+
+      content += '</tr>';
+    }
+  }
+  content += '</tbody></table>';
+  return content;
+}
+
+function generateInfoContent(infoList) {
+  let infoContent = infoList.join('<br>');
+  return infoContent;
+}
+
+module.exports = GeometryViewer;
diff --git a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css
index 75ead26..0800fa4 100644
--- a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css
+++ b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css
@@ -641,3 +641,76 @@ input.editor-checkbox:focus {
 .connection-status-hide {
   display: none;
 }
+
+/* For geometry data viewer panel */
+.sql-editor-geometry-viewer{
+  width: 100%;
+  height: 100%;
+}
+
+.geometry-viewer-container {
+  width: 100%;
+  height: 100%;
+  background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAANElEQVQoU2O8e/fuf2VlZUYGLAAkB5bApggmBteJrAiZjWI0SAJkIrKVxCvAawVeRxLyJgB+Ajc1cwux9wAAAABJRU5ErkJggg==);
+ }
+
+/* For leaflet map background */
+.geometry-viewer-container-plain-background {
+  background: #ffffff;
+}
+
+/* For geometry column button */
+.div-view-geometry-column {
+  float: right;
+  height: 100%;
+  display: flex;
+  display: -webkit-flex;
+  align-items: center;
+  padding-right: 4px;
+}
+
+/* For leaflet popup */
+.leaflet-popup-content-wrapper {
+  border-radius: 2px;
+}
+
+.leaflet-popup-content {
+  margin: 5px;
+  padding: 10px 10px 0;
+  overflow-y: scroll;
+  overflow-x: hidden;
+}
+
+/* For geometry viewer property table */
+.view-geometry-property-table {
+  table-layout: fixed;
+  white-space: nowrap;
+  padding: 0;
+}
+
+.view-geometry-property-table th {
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.view-geometry-property-table td {
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.view-geometry-property-table .td-disabled{
+  color: #999999;
+}
+
+/* For geometry viewer info control */
+.geometry-viewer-info-control {
+  padding: 5px;
+  background: white;
+  border: 2px solid rgba(0, 0, 0, 0.2);
+  background-clip: padding-box;
+  border-radius: 2px;
+}
+
+.geometry-viewer-info-control i{
+  margin: 0 0 0 4px;
+}
diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
index 0fd89c5..6ba3cfa 100644
--- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
+++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js
@@ -15,6 +15,7 @@ define('tools.querytool', [
   'sources/sqleditor/execute_query',
   'sources/sqleditor/query_tool_http_error_handler',
   'sources/sqleditor/filter_dialog',
+  'sources/sqleditor/geometry_viewer',
   'sources/history/index.js',
   'sourcesjsx/history/query_history',
   'react', 'react-dom',
@@ -37,7 +38,7 @@ define('tools.querytool', [
   babelPollyfill, gettext, url_for, $, _, S, alertify, pgAdmin, Backbone, codemirror,
   pgExplain, GridSelector, ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent,
   XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, httpErrorHandler, FilterHandler,
-  HistoryBundle, queryHistory, React, ReactDOM,
+  GeometryViewer, HistoryBundle, queryHistory, React, ReactDOM,
   keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid,
   modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref) {
   /* Return back, this has been called more than once */
@@ -217,7 +218,7 @@ define('tools.querytool', [
         }, 200);
       });
 
-      // Create panels for 'Data Output', 'Explain', 'Messages' and 'History'
+      // Create panels for 'Data Output', 'Explain', 'Messages', 'History' and 'Geometry Viewer'
       var data_output = new pgAdmin.Browser.Panel({
         name: 'data_output',
         title: gettext('Data Output'),
@@ -268,12 +269,23 @@ define('tools.querytool', [
         content: '<div id ="notification_grid" class="sql-editor-notifications" tabindex: "0"></div>',
       });
 
+      var geometry_viewer = new pgAdmin.Browser.Panel({
+        name: 'geometry_viewer',
+        title: gettext('Geometry Viewer'),
+        width: '100%',
+        height: '100%',
+        isCloseable: true,
+        isPrivate: true,
+        content: '<div id ="geometry_viewer_panel" class="sql-editor-geometry-viewer" tabindex: "0"></div>',
+      });
+
       // Load all the created panels
       data_output.load(main_docker);
       explain.load(main_docker);
       messages.load(main_docker);
       history.load(main_docker);
       notifications.load(main_docker);
+      geometry_viewer.load(main_docker);
 
       // Add all the panels to the docker
       self.data_output_panel = main_docker.addPanel('data_output', wcDocker.DOCK.BOTTOM, sql_panel_obj);
@@ -605,6 +617,8 @@ define('tools.querytool', [
      - This plugin is useful for selecting rows using checkbox
      3) RowSelectionModel
      - This plugin is needed by CheckboxSelectColumn plugin to select rows
+     4) Slick.HeaderButtons
+     - This plugin is useful for add buttons in column header
 
      Grid Options:
      -------------
@@ -743,6 +757,9 @@ define('tools.querytool', [
         } else if (c.cell == 'binary') {
           // We do not support editing binary data in SQL editor and data grid.
           options['formatter'] = Slick.Formatters.Binary;
+        } else if (c.cell == 'geometry' || c.cell == 'geography') {
+          // increase width to add 'view' button
+          options['width'] += 28;
         } else {
           options['editor'] = is_editable ? Slick.Editors.pgText :
             Slick.Editors.ReadOnlypgText;
@@ -755,6 +772,13 @@ define('tools.querytool', [
       var gridSelector = new GridSelector();
       grid_columns = self.grid_columns = gridSelector.getColumnDefinitions(grid_columns);
 
+      // add 'view' button in geometry and geography type column header
+      _.each(grid_columns, function (c) {
+        if (c.column_type_internal == 'geometry' || c.column_type_internal == 'geography') {
+          GeometryViewer.add_header_button(c);
+        }
+      });
+
       if (rows_affected) {
         // calculate with for header row column.
         grid_columns[0]['width'] = SqlEditorUtils.calculateColumnWidth(rows_affected);
@@ -817,6 +841,38 @@ define('tools.querytool', [
       grid.registerPlugin(new ActiveCellCapture());
       grid.setSelectionModel(new XCellSelectionModel());
       grid.registerPlugin(gridSelector);
+      var headerButtonsPlugin = new Slick.Plugins.HeaderButtons();
+      headerButtonsPlugin.onCommand.subscribe(function (e, args) {
+        let command = args.command;
+        if (command === 'view-geometries') {
+          let columns = args.grid.getColumns();
+          let columnIndex = columns.indexOf(args.column);
+          let selectedRows = args.grid.getSelectedRows();
+          if (selectedRows.length === 0) {
+            // if no rows are selected, load and render all the rows
+            if (self.handler.has_more_rows) {
+              self.fetch_next_all(function () {
+                // trigger onGridSelectAll manually with new event data.
+                gridSelector.onGridSelectAll.notify(args, new Slick.EventData());
+                let items = args.grid.getData().getItems();
+                GeometryViewer.render_geometries(self.handler, items, columns, columnIndex);
+              });
+            } else {
+              gridSelector.onGridSelectAll.notify(args, new Slick.EventData());
+              let items = args.grid.getData().getItems();
+              GeometryViewer.render_geometries(self.handler, items, columns, columnIndex);
+            }
+          } else {
+            // render selected rows
+            let items = args.grid.getData().getItems();
+            let selectedItems = _.map(selectedRows, function (row) {
+              return items[row];
+            });
+            GeometryViewer.render_geometries(self.handler, selectedItems, columns, columnIndex);
+          }
+        }
+      });
+      grid.registerPlugin(headerButtonsPlugin);
 
       var editor_data = {
         keys: (_.isEmpty(self.handler.primary_keys) && self.handler.has_oids) ? self.handler.oids : self.handler.primary_keys,
@@ -2398,6 +2454,14 @@ define('tools.querytool', [
           case 'bytea[]':
             col_cell = 'binary';
             break;
+          case 'geometry':
+            // PostGIS geometry type
+            col_cell = 'geometry';
+            break;
+          case 'geography':
+            // PostGIS geography type
+            col_cell = 'geography';
+            break;
           default:
             col_cell = 'string';
           }
diff --git a/web/regression/javascript/geometry_viewer/geometry_viewer_spec.js b/web/regression/javascript/geometry_viewer/geometry_viewer_spec.js
new file mode 100644
index 0000000..b11e10c
--- /dev/null
+++ b/web/regression/javascript/geometry_viewer/geometry_viewer_spec.js
@@ -0,0 +1,231 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2018, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import GeometryViewer from 'sources/sqleditor/geometry_viewer';
+
+describe('geometry viewer test', function () {
+
+  describe('geometry viewer add header button test', function () {
+    let add_button = GeometryViewer.add_header_button;
+    it('should add button for geography type', function () {
+      let columnDef = {
+        column_type_internal: 'geography',
+      };
+      add_button(columnDef);
+      expect(columnDef.header).toBeDefined();
+    });
+
+    it('should add button for geometry type', function () {
+      let columnDef = {
+        column_type_internal: 'geometry',
+      };
+      add_button(columnDef);
+      expect(columnDef.header).toBeDefined();
+    });
+  });
+
+  describe('geometry viewer rener geometry test', function () {
+
+    it('should group geometry by srid', function () {
+      // POINT(0 0)
+      let ewkb = '010100000000000000000000000000000000000000';
+      // SRID=32632;POINT(0 0)
+      let ewkb1 = '0101000020787F000000000000000000000000000000000000';
+      let items = [{
+        id: 1,
+        geom: ewkb,
+      }, {
+        id: 1,
+        geom: ewkb,
+      }, {
+        id: 1,
+        geom: ewkb1,
+      }];
+      let columns = [
+        {
+          column_type_internal: 'geometry',
+          field: 'geom',
+        },
+      ];
+      let columnIndex = 0;
+      let result = GeometryViewer.parse_data(items, columns, columnIndex);
+      expect(result.geoJSONs.length).toBe(2);
+    });
+
+
+    it('should support geometry collection', function () {
+      // GEOMETRYCOLLECTION(POINT(2 3),LINESTRING(2 3,3 4))
+      let ewkb = '01070000000200000001010000000000000000000040000000000000084001' +
+        '02000000020000000000000000000040000000000000084000000000000008400000000' +
+        '000001040';
+      let items = [{
+        id: 1,
+        geom: ewkb,
+      }];
+      let columns = [
+        {
+          column_type_internal: 'geometry',
+          field: 'geom',
+        },
+      ];
+      let columnIndex = 0;
+      let result = GeometryViewer.parse_data(items, columns, columnIndex);
+      expect(result.geoJSONs.length).toBe(1);
+    });
+
+    it('should support geometry M', function () {
+      // SRID=4326;MULTIPOINTM(0 0 0,1 2 1)
+      let ewkb = '0104000060E610000002000000010100004000000000000000000000000000' +
+        '00000000000000000000000101000040000000000000F03F00000000000000400000000' +
+        '00000F03F';
+      let items = [{
+        id: 1,
+        geom: ewkb,
+      }];
+      let columns = [
+        {
+          column_type_internal: 'geometry',
+          field: 'geom',
+        },
+      ];
+      let columnIndex = 0;
+      let result = GeometryViewer.parse_data(items, columns, columnIndex);
+      expect(result.geoJSONs.length).toBe(1);
+    });
+
+    it('should support empty geometry', function () {
+      // GEOMETRYCOLLECTION EMPTY
+      let ewkb = '010700000000000000';
+      let items = [{
+        id: 1,
+        geom: ewkb,
+      }];
+      let columns = [
+        {
+          column_type_internal: 'geometry',
+          field: 'geom',
+        },
+      ];
+      let columnIndex = 0;
+      let result = GeometryViewer.parse_data(items, columns, columnIndex);
+      expect(result.geoJSONs.length).toBe(1);
+    });
+
+
+    it('should support mixed geometry type', function () {
+      // GEOMETRYCOLLECTION EMPTY
+      let ewkb = '010700000000000000';
+      // POINT(0 0)
+      let ewkb1 = '010100000000000000000000000000000000000000';
+      // SRID=4326;MULTIPOINTM(0 0 0,1 2 1)
+      let ewkb2 = '0104000060E610000002000000010100004000000000000000000000000000' +
+        '00000000000000000000000101000040000000000000F03F00000000000000400000000' +
+        '00000F03F';
+      let items = [{
+        id: 1,
+        geom: ewkb,
+      }, {
+        id: 1,
+        geom: ewkb1,
+      }, {
+        id: 1,
+        geom: ewkb2,
+      }];
+      let columns = [
+        {
+          column_type_internal: 'geometry',
+          field: 'geom',
+        },
+      ];
+      let columnIndex = 0;
+      let result = GeometryViewer.parse_data(items, columns, columnIndex);
+      expect(result.geoJSONs.length).toBe(2);
+    });
+
+
+    it('should not support 3D geometry', function () {
+      // POINT(0 0 0)
+      let ewkb = '0101000080000000000000F03F000000000000F03F000000000000F03F';
+      let items = [{
+        id: 1,
+        geom: ewkb,
+      }];
+      let columns = [
+        {
+          column_type_internal: 'geometry',
+          field: 'geom',
+        },
+      ];
+      let columnIndex = 0;
+      let result = GeometryViewer.parse_data(items, columns, columnIndex);
+      expect(result.geoJSONs.length).toBe(0);
+    });
+
+    it('should not support 3DM geometry', function () {
+      // POINT(0 0 0 0)
+      let ewkb = '01010000C00000000000000000000000000000000000000000000000000000' +
+        '000000000000';
+      let items = [{
+        id: 1,
+        geom: ewkb,
+      }];
+      let columns = [
+        {
+          column_type_internal: 'geometry',
+          field: 'geom',
+        },
+      ];
+      let columnIndex = 0;
+      let result = GeometryViewer.parse_data(items, columns, columnIndex);
+      expect(result.geoJSONs.length).toBe(0);
+    });
+
+    it('should not support TRIANGLE geometry', function () {
+      // TRIANGLE ((0 0, 0 9, 9 0, 0 0))
+      let ewkb = '01110000000100000004000000000000000000000000000000000000000000' +
+        '00000000000000000000000022400000000000002240000000000000000000000000000' +
+        '000000000000000000000';
+      let items = [{
+        id: 1,
+        geom: ewkb,
+      }];
+      let columns = [
+        {
+          column_type_internal: 'geometry',
+          field: 'geom',
+        },
+      ];
+      let columnIndex = 0;
+      let result = GeometryViewer.parse_data(items, columns, columnIndex);
+      expect(result.geoJSONs.length).toBe(0);
+    });
+
+    it('should limit data size', function () {
+      // POINT(0 0)
+      let ewkb = '010100000000000000000000000000000000000000';
+      let items = [];
+      for (let i = 0; i < 600000; i++) {
+        items.push({
+          id: i,
+          geom: ewkb,
+        });
+      }
+
+      let columns = [
+        {
+          column_type_internal: 'geometry',
+          field: 'geom',
+        },
+      ];
+      let columnIndex = 0;
+      let result = GeometryViewer.parse_data(items, columns, columnIndex);
+      expect(result.geoJSONs.length).toBeLessThan(600000);
+    });
+  });
+});
diff --git a/web/yarn.lock b/web/yarn.lock
index f0ff60b..d3a0384 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -5604,6 +5604,10 @@ lead@^1.0.0:
   dependencies:
     flush-write-stream "^1.0.2"
 
+leaflet@^1.3.3:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.3.3.tgz#5c8f2fd50e4a41ead93ab850dcd9e058811da9b9"
+
 level-codec@~7.0.0:
   version "7.0.1"
   resolved "https://registry.yarnpkg.com/level-codec/-/level-codec-7.0.1.tgz#341f22f907ce0f16763f24bddd681e395a0fb8a7"
@@ -9611,6 +9615,12 @@ window-size@0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
 
+wkx@^0.4.5:
+  version "0.4.5"
+  resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.4.5.tgz#a85e15a6e69d1bfaec2f3c523be3dfa40ab861d0"
+  dependencies:
+    "@types/node" "*"
+
 wordwrap@0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
