Merge pull request #11202 from Monogramm/feat/map-develop

This commit is contained in:
Prssanna Desai 2020-12-29 17:25:41 +05:30 committed by GitHub
commit dc8d6a99d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 266 additions and 6 deletions

96
frappe/geo/utils.py Normal file
View 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)

View file

@ -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",

View file

@ -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;

View file

@ -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: '&copy; <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() {

View file

@ -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);

View file

@ -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">

View file

@ -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;

View file

@ -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: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}
},
});
// Array de duplicate

View 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"
];
}
};

View file

@ -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 {

View 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()