Merge pull request #11202 from Monogramm/feat/map-develop
This commit is contained in:
commit
dc8d6a99d3
11 changed files with 266 additions and 6 deletions
96
frappe/geo/utils.py
Normal file
96
frappe/geo/utils.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(this.map);
|
||||
L.tileLayer(frappe.utils.map_defaults.tiles,
|
||||
frappe.utils.map_defaults.options).addTo(this.map);
|
||||
},
|
||||
|
||||
bind_leaflet_locate_control() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@
|
|||
<a href="#List/{%= doctype %}/Dashboard">{%= __("Dashboard") %}</a></li>
|
||||
<li class="hide list-link" data-view="Image">
|
||||
<a href="#List/{%= doctype %}/Image">{%= __("Images") %}</a></li>
|
||||
<li class="hide list-link" data-view="Map">
|
||||
<a href="#List/{%= doctype %}/Map">{%= __("Map") %}</a></li>
|
||||
<li class="hide list-link" data-view="Gantt">
|
||||
<a href="#List/{%= doctype %}/Gantt">{%= __("Gantt") %}</a></li>
|
||||
<li class="hide tree-link">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Array de duplicate
|
||||
|
|
|
|||
85
frappe/public/js/frappe/views/map/map_view.js
Normal file
85
frappe/public/js/frappe/views/map/map_view.js
Normal file
|
|
@ -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('<div></div>');
|
||||
}
|
||||
|
||||
render_map_view() {
|
||||
this.map_id = frappe.dom.get_unique_id();
|
||||
|
||||
this.$result.html(`<div id="${this.map_id}" class="map-view-container"></div>`);
|
||||
|
||||
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"
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
42
frappe/tests/tests_geo_utils.py
Normal file
42
frappe/tests/tests_geo_utils.py
Normal file
|
|
@ -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()
|
||||
Loading…
Add table
Reference in a new issue