diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py
new file mode 100644
index 0000000000..d94a13ea41
--- /dev/null
+++ b/frappe/geo/utils.py
@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+
+from pymysql import InternalError
+
+
+@frappe.whitelist()
+def get_coords(doctype, filters, type):
+ '''Get a geojson dict representing a doctype.'''
+ filters_sql = get_coords_conditions(doctype, filters)[4:]
+
+ coords = None
+ if type == 'location_field':
+ coords = return_location(doctype, filters_sql)
+ elif type == 'coordinates':
+ coords = return_coordinates(doctype, filters_sql)
+
+ out = convert_to_geojson(type, coords)
+ return out
+
+def convert_to_geojson(type, coords):
+ '''Converts GPS coordinates to geoJSON string.'''
+ geojson = {"type": "FeatureCollection", "features": None}
+
+ if type == 'location_field':
+ geojson['features'] = merge_location_features_in_one(coords)
+ elif type == 'coordinates':
+ geojson['features'] = create_gps_markers(coords)
+
+ return geojson
+
+
+def merge_location_features_in_one(coords):
+ '''Merging all features from location field.'''
+ geojson_dict = []
+ for element in coords:
+ geojson_loc = frappe.parse_json(element['location'])
+ if not geojson_loc:
+ continue
+ for coord in geojson_loc['features']:
+ coord['properties']['name'] = element['name']
+ geojson_dict.append(coord.copy())
+
+ return geojson_dict
+
+
+def create_gps_markers(coords):
+ '''Build Marker based on latitude and longitude.'''
+ geojson_dict = []
+ for i in coords:
+ node = {"type": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": None}}
+ node['properties']['name'] = i.name
+ node['geometry']['coordinates'] = [i.latitude, i.longitude]
+ geojson_dict.append(node.copy())
+
+ return geojson_dict
+
+
+def return_location(doctype, filters_sql):
+ '''Get name and location fields for Doctype.'''
+ if filters_sql:
+ try:
+ coords = frappe.db.sql('''SELECT name, location FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True)
+ except InternalError:
+ frappe.msgprint(frappe._('This Doctype does not contain location fields'), raise_exception=True)
+ return
+ else:
+ coords = frappe.get_all(doctype, fields=['name', 'location'])
+ return coords
+
+
+def return_coordinates(doctype, filters_sql):
+ '''Get name, latitude and longitude fields for Doctype.'''
+ if filters_sql:
+ try:
+ coords = frappe.db.sql('''SELECT name, latitude, longitude FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True)
+ except InternalError:
+ frappe.msgprint(frappe._('This Doctype does not contain latitude and longitude fields'), raise_exception=True)
+ return
+ else:
+ coords = frappe.get_all(doctype, fields=['name', 'latitude', 'longitude'])
+ return coords
+
+
+def get_coords_conditions(doctype, filters=None):
+ '''Returns SQL conditions with user permissions and filters for event queries.'''
+ from frappe.desk.reportview import get_filters_cond
+ if not frappe.has_permission(doctype):
+ frappe.throw(frappe._("Not Permitted"), frappe.PermissionError)
+
+ return get_filters_cond(doctype, filters, [], with_match_conditions=True)
diff --git a/frappe/public/build.json b/frappe/public/build.json
index a3622499d5..d744da98d1 100755
--- a/frappe/public/build.json
+++ b/frappe/public/build.json
@@ -307,6 +307,7 @@
"public/js/frappe/views/calendar/calendar.js",
"public/js/frappe/views/dashboard/dashboard_view.js",
"public/js/frappe/views/image/image_view.js",
+ "public/js/frappe/views/map/map_view.js",
"public/js/frappe/views/kanban/kanban_view.js",
"public/js/frappe/views/inbox/inbox_view.js",
"public/js/frappe/views/file/file_view.js",
diff --git a/frappe/public/css/list.css b/frappe/public/css/list.css
index 5ae77c73ca..88ad147d33 100644
--- a/frappe/public/css/list.css
+++ b/frappe/public/css/list.css
@@ -401,6 +401,13 @@ input.list-row-checkbox {
.pswp__more-item img {
max-height: 100%;
}
+.map-view-container {
+ display: flex;
+ flex-wrap: wrap;
+ width: 100%;
+ height: calc(100vh - 284px);
+ z-index: 0;
+}
.list-paging-area .gantt-view-mode {
margin-left: 15px;
margin-right: 15px;
diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js
index 9dfad09299..9e4d1d82ec 100644
--- a/frappe/public/js/frappe/form/controls/geolocation.js
+++ b/frappe/public/js/frappe/form/controls/geolocation.js
@@ -1,3 +1,5 @@
+frappe.provide('frappe.utils.utils');
+
frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({
horizontal: false,
@@ -90,11 +92,11 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({
});
L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/';
- this.map = L.map(this.map_id).setView([19.0800, 72.8961], 13);
+ this.map = L.map(this.map_id).setView(frappe.utils.map_defaults.center,
+ frappe.utils.map_defaults.zoom);
- L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
- attribution: '© OpenStreetMap contributors'
- }).addTo(this.map);
+ L.tileLayer(frappe.utils.map_defaults.tiles,
+ frappe.utils.map_defaults.options).addTo(this.map);
},
bind_leaflet_locate_control() {
diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js
index bdc7dc0827..83b5a0bdfe 100644
--- a/frappe/public/js/frappe/list/base_list.js
+++ b/frappe/public/js/frappe/list/base_list.js
@@ -693,5 +693,5 @@ class FilterArea {
}
// utility function to validate view modes
-frappe.views.view_modes = ['List', 'Gantt', 'Kanban', 'Calendar', 'Image', 'Inbox', 'Report', 'Dashboard'];
+frappe.views.view_modes = ['List', 'Gantt', 'Kanban', 'Calendar', 'Image', 'Map', 'Inbox', 'Report', 'Dashboard'];
frappe.views.is_valid = view_mode => frappe.views.view_modes.includes(view_mode);
diff --git a/frappe/public/js/frappe/list/list_sidebar.html b/frappe/public/js/frappe/list/list_sidebar.html
index dcbbe7ac5e..c5b75782b5 100644
--- a/frappe/public/js/frappe/list/list_sidebar.html
+++ b/frappe/public/js/frappe/list/list_sidebar.html
@@ -30,6 +30,8 @@
{%= __("Dashboard") %}
{%= __("Images") %}
+
+ {%= __("Map") %}
{%= __("Gantt") %}
diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js
index 2a25e64bf3..5db18dd280 100644
--- a/frappe/public/js/frappe/list/list_sidebar.js
+++ b/frappe/public/js/frappe/list/list_sidebar.js
@@ -89,6 +89,14 @@ frappe.views.ListSidebar = class ListSidebar {
this.sidebar.find('.list-link[data-view="Image"]').removeClass('hide');
show_list_link = true;
}
+
+ if (this.list_view.settings.get_coords_method ||
+ (this.list_view.meta.fields.find(i => i.fieldname === "latitude") &&
+ this.list_view.meta.fields.find(i => i.fieldname === "longitude")) ||
+ (this.list_view.meta.fields.find(i => i.fieldname === 'location' && i.fieldtype == 'Geolocation'))) {
+ this.sidebar.find('.list-link[data-view="Map"]').removeClass('hide');
+ show_list_link = true;
+ }
if (show_list_link) {
this.sidebar.find('.list-link[data-view="List"]').removeClass('hide');
@@ -209,7 +217,7 @@ frappe.views.ListSidebar = class ListSidebar {
let email_account = (account.email_id == "All Accounts") ? "All Accounts" : account.email_account;
let route = ["List", "Communication", "Inbox", email_account].join('/');
let display_name = ["All Accounts", "Sent Mail", "Spam", "Trash"].includes(account.email_id) ? __(account.email_id) : account.email_id;
-
+
if (!divider) {
this.get_divider().appendTo($dropdown);
divider = true;
diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js
index f8f25293b3..32be29df92 100644
--- a/frappe/public/js/frappe/utils/utils.js
+++ b/frappe/public/js/frappe/utils/utils.js
@@ -1051,6 +1051,14 @@ Object.assign(frappe.utils, {
return number_system_map[country];
},
+ map_defaults: {
+ center: [19.0800, 72.8961],
+ zoom: 13,
+ tiles: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
+ options: {
+ attribution: '© OpenStreetMap contributors'
+ }
+ },
});
// Array de duplicate
diff --git a/frappe/public/js/frappe/views/map/map_view.js b/frappe/public/js/frappe/views/map/map_view.js
new file mode 100644
index 0000000000..878311b9bd
--- /dev/null
+++ b/frappe/public/js/frappe/views/map/map_view.js
@@ -0,0 +1,85 @@
+/**
+ * frappe.views.MapView
+ */
+frappe.provide('frappe.utils.utils');
+frappe.provide("frappe.views");
+
+frappe.views.MapView = class MapView extends frappe.views.ListView {
+ get view_name() {
+ return 'Map';
+ }
+
+ setup_defaults() {
+ super.setup_defaults();
+ this.page_title = __('{0} Map', [this.page_title]);
+ }
+
+ setup_view() {
+ }
+
+ on_filter_change() {
+ this.get_coords();
+ }
+
+ render() {
+ this.get_coords()
+ .then(() => {
+ this.render_map_view();
+ });
+ this.$paging_area.find('.level-left').append('');
+ }
+
+ render_map_view() {
+ this.map_id = frappe.dom.get_unique_id();
+
+ this.$result.html(``);
+
+ L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/';
+ this.map = L.map(this.map_id).setView(frappe.utils.map_defaults.center,
+ frappe.utils.map_defaults.zoom);
+
+ L.tileLayer(frappe.utils.map_defaults.tiles,
+ frappe.utils.map_defaults.options).addTo(this.map);
+
+ L.control.scale().addTo(this.map);
+ if (this.coords.features && this.coords.features.length) {
+ this.coords.features.forEach(
+ coords => L.geoJSON(coords).bindPopup(coords.properties.name).addTo(this.map)
+ );
+ let lastCoords = this.coords.features[0].geometry.coordinates.reverse();
+ this.map.panTo(lastCoords, 8);
+ }
+ }
+
+ get_coords() {
+ let get_coords_method = this.settings && this.settings.get_coords_method || 'frappe.geo.utils.get_coords';
+
+ if (cur_list.meta.fields.find(i => i.fieldname === 'location' && i.fieldtype === 'Geolocation')) {
+ this.type = 'location_field';
+ } else if (cur_list.meta.fields.find(i => i.fieldname === "latitude") &&
+ cur_list.meta.fields.find(i => i.fieldname === "longitude")) {
+ this.type = 'coordinates';
+ }
+ return frappe.call({
+ method: get_coords_method,
+ args: {
+ doctype: this.doctype,
+ filters: cur_list.filter_area.get(),
+ type: this.type
+ }
+ }).then(r => {
+ this.coords = r.message;
+
+ });
+ }
+
+
+ get required_libs() {
+ return [
+ "assets/frappe/js/lib/leaflet/leaflet.css",
+ "assets/frappe/js/lib/leaflet/leaflet.js"
+ ];
+ }
+
+
+};
diff --git a/frappe/public/less/list.less b/frappe/public/less/list.less
index 7e57d23fdc..fe2e1cf48d 100644
--- a/frappe/public/less/list.less
+++ b/frappe/public/less/list.less
@@ -483,6 +483,15 @@ input.list-check-all, input.list-row-checkbox {
padding-top: 2px;
}
+// map
+.map-view-container {
+ display: flex;
+ flex-wrap: wrap;
+ width: 100%;
+ height: calc(100vh - 284px);
+ z-index: 0;
+}
+
// list view
.modal-body {
diff --git a/frappe/tests/tests_geo_utils.py b/frappe/tests/tests_geo_utils.py
new file mode 100644
index 0000000000..2067a6aa97
--- /dev/null
+++ b/frappe/tests/tests_geo_utils.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import unittest
+
+import frappe
+from frappe.geo.utils import get_coords
+
+
+class TestGeoUtils(unittest.TestCase):
+ def setUp(self):
+ self.todo = frappe.get_doc(
+ dict(doctype='ToDo', description='Test description', assigned_by='Administrator')).insert()
+
+ self.test_location_dict = {'type': 'FeatureCollection', 'features': [
+ {'type': 'Feature', 'properties': {}, "geometry": {'type': 'Point', 'coordinates': [49.20433, 55.753395]}}]}
+ self.test_location = frappe.get_doc({'name': 'Test Location', 'doctype': 'Location',
+ 'location': str(self.test_location_dict)})
+
+ self.test_filter_exists = [['Location', 'name', 'like', '%Test Location%']]
+ self.test_filter_not_exists = [['Location', 'name', 'like', '%Test Location Not exists%']]
+ self.test_filter_todo = [['ToDo', 'description', 'like', '%Test description%']]
+
+ def test_get_coords_location_with_filter_exists(self):
+ coords = get_coords('Location', self.test_filter_exists, 'location_field')
+ self.assertEqual(self.test_location_dict['features'][0]['geometry'], coords['features'][0]['geometry'])
+
+ def test_get_coords_location_with_filter_not_exists(self):
+ coords = get_coords('Location', self.test_filter_not_exists, 'location_field')
+ self.assertEqual(coords, {'type': 'FeatureCollection', 'features': []})
+
+ def test_get_coords_from_not_existable_location(self):
+ self.assertRaises(frappe.ValidationError, get_coords, 'ToDo', self.test_filter_todo, 'location_field')
+
+ def test_get_coords_from_not_existable_coords(self):
+ self.assertRaises(frappe.ValidationError, get_coords, 'ToDo', self.test_filter_todo, 'coordinates')
+
+ def tearDown(self):
+ self.todo.delete()