From 31f1e085e66eaa2ffc1e8e9bad08638aba08ee55 Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Fri, 10 Jun 2022 07:17:40 +0200 Subject: [PATCH 01/29] Use webresource for resource registration --- LICENSE.rst | 2 +- rollup.conf.js => js/rollup.conf.js | 10 +- scripts/rollup.sh | 2 +- src/cone/maps/__init__.py | 47 +---- src/cone/maps/browser/__init__.py | 171 +++++++++++++++++- .../browser/static/{ => maps}/cone.maps.js | 0 .../static/{ => maps}/cone.maps.min.js | 0 7 files changed, 176 insertions(+), 56 deletions(-) rename rollup.conf.js => js/rollup.conf.js (85%) rename src/cone/maps/browser/static/{ => maps}/cone.maps.js (100%) rename src/cone/maps/browser/static/{ => maps}/cone.maps.min.js (100%) diff --git a/LICENSE.rst b/LICENSE.rst index 0d57f53..c0cfca3 100644 --- a/LICENSE.rst +++ b/LICENSE.rst @@ -1,7 +1,7 @@ License ======= -Copyright (c) 2021, Cone Contributors +Copyright (c) 2021-2022, Cone Contributors All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/rollup.conf.js b/js/rollup.conf.js similarity index 85% rename from rollup.conf.js rename to js/rollup.conf.js index 467e871..8896ba8 100644 --- a/rollup.conf.js +++ b/js/rollup.conf.js @@ -1,11 +1,7 @@ import cleanup from 'rollup-plugin-cleanup'; import {terser} from 'rollup-plugin-terser'; -const out_dir = 'src/cone/maps/browser/static'; - -const outro = ` -window.cone_maps = exports; -`; +const out_dir = 'src/cone/maps/browser/static/maps'; export default args => { let conf = { @@ -15,8 +11,8 @@ export default args => { ], output: [{ file: `${out_dir}/cone.maps.js`, + name: 'cone_maps', format: 'iife', - outro: outro, globals: { jquery: 'jQuery' }, @@ -30,11 +26,11 @@ export default args => { if (args.configDebug !== true) { conf.output.push({ file: `${out_dir}/cone.maps.min.js`, + name: 'cone_maps', format: 'iife', plugins: [ terser() ], - outro: outro, globals: { jquery: 'jQuery' }, diff --git a/scripts/rollup.sh b/scripts/rollup.sh index d071103..97fa725 100755 --- a/scripts/rollup.sh +++ b/scripts/rollup.sh @@ -1,3 +1,3 @@ #!/bin/bash -node_modules/rollup/dist/bin/rollup --config rollup.conf.js "$@" +node_modules/rollup/dist/bin/rollup --config js/rollup.conf.js "$@" diff --git a/src/cone/maps/__init__.py b/src/cone/maps/__init__.py index 45bc3b9..452a285 100644 --- a/src/cone/maps/__init__.py +++ b/src/cone/maps/__init__.py @@ -1,6 +1,6 @@ from cone.app import cfg from cone.app import main_hook -from cone.maps.browser import static_resources +from cone.maps.browser import configure_resources import logging @@ -11,54 +11,11 @@ def initialize_maps(config, global_config, settings): # application startup initialization - # ignore yafowil leaflet dependencies if cone.maps is installed - cfg.yafowil.js_skip.add('yafowil.widget.location.dependencies') - cfg.yafowil.css_skip.add('yafowil.widget.location.dependencies') - - # resources - if settings.get('cone.maps.public', 'false') == 'true': - css_res = cfg.css.public - js_res = cfg.js.public - else: - css_res = cfg.css.protected - js_res = cfg.js.protected - - # leaflet core - css_res.append('maps-static/leaflet/leaflet.css') - js_res.append('maps-static/leaflet/leaflet.js') - - # Leaflet.TileLayer.NoGap - if settings.get('cone.maps.nogap', 'false') == 'true': - js_res.append('maps-static/leaflet-nogap/L.TileLayer.NoGap.js') - - # leaflet-geosearch - if settings.get('cone.maps.geosearch', 'false') == 'true': - css_res.append('maps-static/leaflet-geosearch/geosearch.css') - js_res.append('maps-static/leaflet-geosearch/geosearch.umd.js') - - # Leaflet.markercluster - if settings.get('cone.maps.markercluster', 'false') == 'true': - css_res.append('maps-static/leaflet-markercluster/MarkerCluster.css') - css_res.append('maps-static/leaflet-markercluster/MarkerCluster.Default.css') - js_res.append('maps-static/leaflet-markercluster/leaflet.markercluster.js') - - # Leaflet-active-area - if settings.get('cone.maps.activearea', 'false') == 'true': - js_res.append('maps-static/leaflet-activearea/leaflet.activearea.js') - - # proj4js and Proj4Leaflet - if settings.get('cone.maps.proj4', 'false') == 'true': - js_res.append('maps-static/proj4js/proj4.js') - js_res.append('maps-static/leaflet-proj4/proj4leaflet.js') - - # cone maps - js_res.append('maps-static/cone.maps.js') - # add translation config.add_translation_dirs('cone.maps:locale/') # static resources - config.add_view(static_resources, name='maps-static') + configure_resources(settings) # scan browser package config.scan('cone.maps.browser') diff --git a/src/cone/maps/browser/__init__.py b/src/cone/maps/browser/__init__.py index fc31959..2a39844 100644 --- a/src/cone/maps/browser/__init__.py +++ b/src/cone/maps/browser/__init__.py @@ -1,4 +1,171 @@ -from pyramid.static import static_view +from cone.app.browser.resources import resources +from cone.app.browser.resources import set_resource_include +import webresource as wr +import os -static_resources = static_view('static', use_subpath=True) +resources_dir = os.path.join(os.path.dirname(__file__), 'static') + + +# leaflet core +leaflet_resources = wr.ResourceGroup( + name='cone.maps-leaflet', + directory=os.path.join(resources_dir, 'leaflet'), + path='leaflet', + group=resources +) +leaflet_resources.add(wr.ScriptResource( + name='leaflet-js', + resource='leaflet-src.js', + compressed='leaflet.js' +)) +leaflet_resources.add(wr.StyleResource( + name='leaflet-css', + resource='leaflet.css' +)) + +# Leaflet.TileLayer.NoGap +leaflet_nogap_resources = wr.ResourceGroup( + name='cone.maps-leaflet-nogap', + directory=os.path.join(resources_dir, 'leaflet-nogap'), + path='leaflet-nogap', + group=resources +) +leaflet_nogap_resources.add(wr.ScriptResource( + name='leaflet-nogap-js', + depends='leaflet-js', + resource='L.TileLayer.NoGap.js' +)) + +# leaflet-geosearch +leaflet_geosearch_resources = wr.ResourceGroup( + name='cone.maps-leaflet-geosearch', + directory=os.path.join(resources_dir, 'leaflet-geosearch'), + path='leaflet-geosearch', + group=resources +) +leaflet_geosearch_resources.add(wr.ScriptResource( + name='leaflet-geosearch-js', + depends='leaflet-js', + resource='geosearch.umd.js' +)) +leaflet_geosearch_resources.add(wr.StyleResource( + name='leaflet-geosearch-css', + depends='leaflet-css', + resource='geosearch.css' +)) + +# Leaflet.markercluster +leaflet_markercluster_resources = wr.ResourceGroup( + name='cone.maps-leaflet-markercluster', + directory=os.path.join(resources_dir, 'leaflet-markercluster'), + path='leaflet-markercluster', + group=resources +) +leaflet_markercluster_resources.add(wr.ScriptResource( + name='leaflet-markercluster-js', + depends='leaflet-js', + resource='leaflet.markercluster-src.js', + compressed='leaflet.markercluster.js' +)) +leaflet_markercluster_resources.add(wr.StyleResource( + name='leaflet-markercluster-css', + depends='leaflet-css', + resource='MarkerCluster.css' +)) +leaflet_markercluster_resources.add(wr.StyleResource( + name='leaflet-markercluster-default-css', + depends='leaflet-markercluster-css', + resource='MarkerCluster.Default.css' +)) + +# Leaflet-active-area +leaflet_activearea_resources = wr.ResourceGroup( + name='cone.maps-leaflet-activearea', + directory=os.path.join(resources_dir, 'leaflet-activearea'), + path='leaflet-activearea', + group=resources +) +leaflet_activearea_resources.add(wr.ScriptResource( + name='leaflet-activearea-js', + depends='leaflet-js', + resource='leaflet.activearea.js' +)) + +# proj4js +proj4_resources = wr.ResourceGroup( + name='cone.maps-proj4', + directory=os.path.join(resources_dir, 'proj4'), + path='proj4', + group=resources +) +proj4_resources.add(wr.ScriptResource( + name='proj4-js', + resource='proj4-src.js', + compressed='proj4.js' +)) + +# Proj4Leaflet +leaflet_proj4_resources = wr.ResourceGroup( + name='cone.maps-leaflet-proj4', + directory=os.path.join(resources_dir, 'leaflet-proj4'), + path='leaflet-proj4', + group=resources +) +leaflet_proj4_resources.add(wr.ScriptResource( + name='leaflet-proj4-js', + depends=['leaflet-js', 'proj4-js'], + resource='proj4leaflet.js' +)) + +# cone maps +cone_maps_resources = wr.ResourceGroup( + name='cone.maps-maps', + directory=os.path.join(resources_dir, 'maps'), + path='maps', + group=resources +) +cone_maps_resources.add(wr.ScriptResource( + name='cone-maps-js', + depends='leaflet-js', + resource='cone.maps.js', + compressed='cone.maps.min.js' +)) + + +def configure_resources(settings): + def included(name): + return settings.get(name, 'false') == 'true' + + include = True if included('cone.maps.public') else 'authenticated' + + # leaflet core + set_resource_include(settings, 'leaflet-js', include) + set_resource_include(settings, 'leaflet-css', include) + + # Leaflet.TileLayer.NoGap + nogap_include = include if included('cone.maps.nogap') else False + set_resource_include(settings, 'leaflet-nogap-js', nogap_include) + + # leaflet-geosearch + geosearch_include = include if included('cone.maps.geosearch') else False + set_resource_include(settings, 'leaflet-geosearch-js', geosearch_include) + set_resource_include(settings, 'leaflet-geosearch-css', geosearch_include) + + # Leaflet.markercluster + mc_include = include if included('cone.maps.markercluster') else False + set_resource_include(settings, 'leaflet-markercluster-js', mc_include) + set_resource_include(settings, 'leaflet-markercluster-css', mc_include) + set_resource_include(settings, 'leaflet-markercluster-default-css', mc_include) + + # Leaflet-active-area + activearea_include = include if included('cone.maps.activearea') else False + set_resource_include(settings, 'leaflet-activearea-js', activearea_include) + + # proj4js and Proj4Leaflet + proj4_include = include if included('cone.maps.proj4') else False + set_resource_include(settings, 'proj4-js', proj4_include) + set_resource_include(settings, 'leaflet-proj4-js', proj4_include) + + # cone maps + set_resource_include(settings, 'cone-maps-js', include) diff --git a/src/cone/maps/browser/static/cone.maps.js b/src/cone/maps/browser/static/maps/cone.maps.js similarity index 100% rename from src/cone/maps/browser/static/cone.maps.js rename to src/cone/maps/browser/static/maps/cone.maps.js diff --git a/src/cone/maps/browser/static/cone.maps.min.js b/src/cone/maps/browser/static/maps/cone.maps.min.js similarity index 100% rename from src/cone/maps/browser/static/cone.maps.min.js rename to src/cone/maps/browser/static/maps/cone.maps.min.js From 02bb5cfeb29dbc48106bb59bd3e855a8f8be45bc Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Fri, 10 Jun 2022 07:43:18 +0200 Subject: [PATCH 02/29] remove superfluous import --- src/cone/maps/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cone/maps/__init__.py b/src/cone/maps/__init__.py index 452a285..54c6990 100644 --- a/src/cone/maps/__init__.py +++ b/src/cone/maps/__init__.py @@ -1,4 +1,3 @@ -from cone.app import cfg from cone.app import main_hook from cone.maps.browser import configure_resources import logging From b5fb40dbbea2ee401f195a6dd9e31b3646e33e82 Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Sat, 11 Jun 2022 14:44:58 +0200 Subject: [PATCH 03/29] test resources --- src/cone/maps/browser/__init__.py | 4 +- src/cone/maps/tests.py | 244 +++++++++++++++++++++++++++++- 2 files changed, 244 insertions(+), 4 deletions(-) diff --git a/src/cone/maps/browser/__init__.py b/src/cone/maps/browser/__init__.py index 2a39844..4f438ae 100644 --- a/src/cone/maps/browser/__init__.py +++ b/src/cone/maps/browser/__init__.py @@ -95,8 +95,8 @@ # proj4js proj4_resources = wr.ResourceGroup( name='cone.maps-proj4', - directory=os.path.join(resources_dir, 'proj4'), - path='proj4', + directory=os.path.join(resources_dir, 'proj4js'), + path='proj4js', group=resources ) proj4_resources.add(wr.ScriptResource( diff --git a/src/cone/maps/tests.py b/src/cone/maps/tests.py index 94dfc1a..1aee947 100644 --- a/src/cone/maps/tests.py +++ b/src/cone/maps/tests.py @@ -1,7 +1,10 @@ from cone.app import testing -from cone.tile.tests import TileTestCase +from cone.app.browser.resources import RESOURCE_INCLUDES_KEY from cone.app.model import BaseNode +from cone.maps import browser from cone.maps.browser.map import MapTile +from cone.tile.tests import TileTestCase +import os import sys import unittest @@ -14,8 +17,11 @@ def make_app(self, **kw): }) +maps_layer = MapsLayer() + + class TestMapsTile(TileTestCase): - layer = MapsLayer() + layer = maps_layer def test_map_tile(self): model = BaseNode(name='map') @@ -38,6 +44,240 @@ def test_map_tile(self): self.assertTrue(res.find('data-map-groups-source=') > -1) +def np(path): + return path.replace('/', os.path.sep) + + +class TestResources(unittest.TestCase): + layer = maps_layer + + def test_leaflet_resources(self): + resources_ = browser.leaflet_resources + self.assertTrue(resources_.directory.endswith(np('/static/leaflet'))) + self.assertEqual(resources_.name, 'cone.maps-leaflet') + self.assertEqual(resources_.path, 'leaflet') + + scripts = resources_.scripts + self.assertEqual(len(scripts), 1) + + self.assertTrue(scripts[0].directory.endswith(np('/static/leaflet'))) + self.assertEqual(scripts[0].path, 'leaflet') + self.assertEqual(scripts[0].file_name, 'leaflet.js') + self.assertTrue(os.path.exists(scripts[0].file_path)) + + styles = resources_.styles + self.assertEqual(len(styles), 1) + + self.assertTrue(styles[0].directory.endswith(np('/static/leaflet'))) + self.assertEqual(styles[0].path, 'leaflet') + self.assertEqual(styles[0].file_name, 'leaflet.css') + self.assertTrue(os.path.exists(styles[0].file_path)) + + def test_leaflet_nogap_resources(self): + resources_ = browser.leaflet_nogap_resources + self.assertTrue(resources_.directory.endswith(np('/static/leaflet-nogap'))) + self.assertEqual(resources_.name, 'cone.maps-leaflet-nogap') + self.assertEqual(resources_.path, 'leaflet-nogap') + + scripts = resources_.scripts + self.assertEqual(len(scripts), 1) + + self.assertTrue(scripts[0].directory.endswith(np('/static/leaflet-nogap'))) + self.assertEqual(scripts[0].path, 'leaflet-nogap') + self.assertEqual(scripts[0].file_name, 'L.TileLayer.NoGap.js') + self.assertTrue(os.path.exists(scripts[0].file_path)) + + styles = resources_.styles + self.assertEqual(len(styles), 0) + + def test_leaflet_geosearch_resources(self): + resources_ = browser.leaflet_geosearch_resources + self.assertTrue(resources_.directory.endswith(np('/static/leaflet-geosearch'))) + self.assertEqual(resources_.name, 'cone.maps-leaflet-geosearch') + self.assertEqual(resources_.path, 'leaflet-geosearch') + + scripts = resources_.scripts + self.assertEqual(len(scripts), 1) + + self.assertTrue(scripts[0].directory.endswith(np('/static/leaflet-geosearch'))) + self.assertEqual(scripts[0].path, 'leaflet-geosearch') + self.assertEqual(scripts[0].file_name, 'geosearch.umd.js') + self.assertTrue(os.path.exists(scripts[0].file_path)) + + styles = resources_.styles + self.assertEqual(len(styles), 1) + + self.assertTrue(styles[0].directory.endswith(np('/static/leaflet-geosearch'))) + self.assertEqual(styles[0].path, 'leaflet-geosearch') + self.assertEqual(styles[0].file_name, 'geosearch.css') + self.assertTrue(os.path.exists(styles[0].file_path)) + + def test_leaflet_markercluster_resources(self): + resources_ = browser.leaflet_markercluster_resources + self.assertTrue(resources_.directory.endswith(np('/static/leaflet-markercluster'))) + self.assertEqual(resources_.name, 'cone.maps-leaflet-markercluster') + self.assertEqual(resources_.path, 'leaflet-markercluster') + + scripts = resources_.scripts + self.assertEqual(len(scripts), 1) + + self.assertTrue(scripts[0].directory.endswith(np('/static/leaflet-markercluster'))) + self.assertEqual(scripts[0].path, 'leaflet-markercluster') + self.assertEqual(scripts[0].file_name, 'leaflet.markercluster.js') + self.assertTrue(os.path.exists(scripts[0].file_path)) + + styles = resources_.styles + self.assertEqual(len(styles), 2) + + self.assertTrue(styles[0].directory.endswith(np('/static/leaflet-markercluster'))) + self.assertEqual(styles[0].path, 'leaflet-markercluster') + self.assertEqual(styles[0].file_name, 'MarkerCluster.css') + self.assertTrue(os.path.exists(styles[0].file_path)) + + self.assertTrue(styles[1].directory.endswith(np('/static/leaflet-markercluster'))) + self.assertEqual(styles[1].path, 'leaflet-markercluster') + self.assertEqual(styles[1].file_name, 'MarkerCluster.Default.css') + self.assertTrue(os.path.exists(styles[1].file_path)) + + def test_leaflet_activearea_resources(self): + resources_ = browser.leaflet_activearea_resources + self.assertTrue(resources_.directory.endswith(np('/static/leaflet-activearea'))) + self.assertEqual(resources_.name, 'cone.maps-leaflet-activearea') + self.assertEqual(resources_.path, 'leaflet-activearea') + + scripts = resources_.scripts + self.assertEqual(len(scripts), 1) + + self.assertTrue(scripts[0].directory.endswith(np('/static/leaflet-activearea'))) + self.assertEqual(scripts[0].path, 'leaflet-activearea') + self.assertEqual(scripts[0].file_name, 'leaflet.activearea.js') + self.assertTrue(os.path.exists(scripts[0].file_path)) + + styles = resources_.styles + self.assertEqual(len(styles), 0) + + def test_proj4_resources(self): + resources_ = browser.proj4_resources + self.assertTrue(resources_.directory.endswith(np('/static/proj4js'))) + self.assertEqual(resources_.name, 'cone.maps-proj4') + self.assertEqual(resources_.path, 'proj4js') + + scripts = resources_.scripts + self.assertEqual(len(scripts), 1) + + self.assertTrue(scripts[0].directory.endswith(np('/static/proj4js'))) + self.assertEqual(scripts[0].path, 'proj4js') + self.assertEqual(scripts[0].file_name, 'proj4.js') + self.assertTrue(os.path.exists(scripts[0].file_path)) + + styles = resources_.styles + self.assertEqual(len(styles), 0) + + def test_leaflet_proj4_resources(self): + resources_ = browser.leaflet_proj4_resources + self.assertTrue(resources_.directory.endswith(np('/static/leaflet-proj4'))) + self.assertEqual(resources_.name, 'cone.maps-leaflet-proj4') + self.assertEqual(resources_.path, 'leaflet-proj4') + + scripts = resources_.scripts + self.assertEqual(len(scripts), 1) + + self.assertTrue(scripts[0].directory.endswith(np('/static/leaflet-proj4'))) + self.assertEqual(scripts[0].path, 'leaflet-proj4') + self.assertEqual(scripts[0].file_name, 'proj4leaflet.js') + self.assertTrue(os.path.exists(scripts[0].file_path)) + + styles = resources_.styles + self.assertEqual(len(styles), 0) + + def test_cone_maps_resources(self): + resources_ = browser.cone_maps_resources + self.assertTrue(resources_.directory.endswith(np('/static/maps'))) + self.assertEqual(resources_.name, 'cone.maps-maps') + self.assertEqual(resources_.path, 'maps') + + scripts = resources_.scripts + self.assertEqual(len(scripts), 1) + + self.assertTrue(scripts[0].directory.endswith(np('/static/maps'))) + self.assertEqual(scripts[0].path, 'maps') + self.assertEqual(scripts[0].file_name, 'cone.maps.min.js') + self.assertTrue(os.path.exists(scripts[0].file_path)) + + styles = resources_.styles + self.assertEqual(len(styles), 0) + + def test_configure_resources(self): + configure_resources = browser.configure_resources + + settings = { + 'cone.maps.public': 'false' + } + configure_resources(settings) + self.assertEqual(settings[RESOURCE_INCLUDES_KEY], { + 'leaflet-js': 'authenticated', + 'leaflet-css': 'authenticated', + 'leaflet-nogap-js': False, + 'leaflet-geosearch-js': False, + 'leaflet-geosearch-css': False, + 'leaflet-markercluster-js': False, + 'leaflet-markercluster-css': False, + 'leaflet-markercluster-default-css': False, + 'leaflet-activearea-js': False, + 'proj4-js': False, + 'leaflet-proj4-js': False, + 'cone-maps-js': 'authenticated' + }) + + settings = { + 'cone.maps.public': 'false', + 'cone.maps.nogap': 'true', + 'cone.maps.geosearch': 'true', + 'cone.maps.markercluster': 'true', + 'cone.maps.activearea': 'true', + 'cone.maps.proj4': 'true' + } + configure_resources(settings) + self.assertEqual(settings[RESOURCE_INCLUDES_KEY], { + 'leaflet-js': 'authenticated', + 'leaflet-css': 'authenticated', + 'leaflet-nogap-js': 'authenticated', + 'leaflet-geosearch-js': 'authenticated', + 'leaflet-geosearch-css': 'authenticated', + 'leaflet-markercluster-js': 'authenticated', + 'leaflet-markercluster-css': 'authenticated', + 'leaflet-markercluster-default-css': 'authenticated', + 'leaflet-activearea-js': 'authenticated', + 'proj4-js': 'authenticated', + 'leaflet-proj4-js': 'authenticated', + 'cone-maps-js': 'authenticated' + }) + + settings = { + 'cone.maps.public': 'true', + 'cone.maps.nogap': 'true', + 'cone.maps.geosearch': 'true', + 'cone.maps.markercluster': 'true', + 'cone.maps.activearea': 'true', + 'cone.maps.proj4': 'true' + } + configure_resources(settings) + self.assertEqual(settings[RESOURCE_INCLUDES_KEY], { + 'leaflet-js': True, + 'leaflet-css': True, + 'leaflet-nogap-js': True, + 'leaflet-geosearch-js': True, + 'leaflet-geosearch-css': True, + 'leaflet-markercluster-js': True, + 'leaflet-markercluster-css': True, + 'leaflet-markercluster-default-css': True, + 'leaflet-activearea-js': True, + 'proj4-js': True, + 'leaflet-proj4-js': True, + 'cone-maps-js': True + }) + + def run_tests(): from cone.maps import tests from zope.testrunner.runner import Runner From 618b9fe7c8621bd3468ecd2c5ec48c2cda9f1245 Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Sat, 11 Jun 2022 15:45:35 +0200 Subject: [PATCH 04/29] configure resources first in main hook --- src/cone/maps/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cone/maps/__init__.py b/src/cone/maps/__init__.py index 54c6990..fd02355 100644 --- a/src/cone/maps/__init__.py +++ b/src/cone/maps/__init__.py @@ -10,11 +10,11 @@ def initialize_maps(config, global_config, settings): # application startup initialization - # add translation - config.add_translation_dirs('cone.maps:locale/') - # static resources configure_resources(settings) + # add translation + config.add_translation_dirs('cone.maps:locale/') + # scan browser package config.scan('cone.maps.browser') From c4ce737ed248003afca719ac5df8097e7c425396 Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Wed, 27 Jul 2022 12:43:53 +0200 Subject: [PATCH 05/29] Implement static map markers. Fix control layers creation in ``Map.create_controls``. --- CHANGES.rst | 6 ++++- README.rst | 3 +++ js/src/map.js | 17 +++++++++----- package.json | 15 +++++-------- scripts/install_js.sh | 4 ++-- src/cone/maps/browser/map.py | 18 +++++++++++++-- .../maps/browser/static/maps/cone.maps.js | 22 ++++++++++--------- .../maps/browser/static/maps/cone.maps.min.js | 2 +- 8 files changed, 57 insertions(+), 30 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7802474..931031f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,11 @@ Changes 0.2 (unreleased) ---------------- -- No changes yet. +- Fix control layers creation in ``Map.create_controls``. + [rnix] + +- Implement static map markers. + [rnix] 0.1 (2021-11-21) diff --git a/README.rst b/README.rst index 6b4e1c4..b37ee7d 100644 --- a/README.rst +++ b/README.rst @@ -57,6 +57,9 @@ can be used as starting point for complex custom maps. """See ``cone.maps.browser.map`` for available tile options. """ +Not that ``cone.maps`` defines by default no height for maps. This must be +done explicitely using CSS, or via custom JS map class. + Resources --------- diff --git a/js/src/map.js b/js/src/map.js index a3b3145..6e1f235 100644 --- a/js/src/map.js +++ b/js/src/map.js @@ -43,10 +43,10 @@ export class Map { this.default_zoom = elem.data('map-zoom'); this.map_options = elem.data('map-options'); this.control_options = elem.data('map-control-options'); - this.markers = elem.data('data-map-markers'); - this.markers_source = elem.data('data-map-markers-source'); - this.marker_groups = elem.data('data-map-groups'); - this.marker_groups_source = elem.data('data-map-groups-source'); + this.markers = elem.data('map-markers'); + this.markers_source = elem.data('map-markers-source'); + this.marker_groups = elem.data('map-groups'); + this.marker_groups_source = elem.data('map-groups-source'); this.create(); elem.data('map-instance', this); @@ -56,6 +56,7 @@ export class Map { this.create_map(); this.create_controls(); this.create_layers(); + this.create_markers(); } create_map() { @@ -66,7 +67,7 @@ export class Map { create_controls() { let base_maps = [], overlay_maps = []; - this.map_layers = new L.control.Layers( + this.map_layers = new L.Control.Layers( base_maps, overlay_maps, this.control_options @@ -99,4 +100,10 @@ export class Map { remove_layer(layer) { this.map.removeLayer(layer); } + + create_markers() { + for (let m of this.markers) { + new L.Marker(m.latlng, m.options).addTo(this.map); + } + } } diff --git a/package.json b/package.json index 2d29f28..9a32ece 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,13 @@ { - "name": "cone.maps", - "version": "0.1.0-dev", "devDependencies": { - "karma": "^4.4.1", - "karma-chrome-launcher": "^3.1.0", - "karma-coverage": "^2.0.3", + "karma": "^6.4.0", + "karma-chrome-launcher": "^3.1.1", + "karma-coverage": "^2.2.0", "karma-module-resolver-preprocessor": "^1.1.3", "karma-qunit": "^4.1.2", - "qunit": "^2.17.2", - "rollup": "^2.60.0", + "qunit": "^2.19.1", + "rollup": "^2.77.2", "rollup-plugin-cleanup": "^3.2.1", "rollup-plugin-terser": "^7.0.2" - }, - "dependencies": {} + } } diff --git a/scripts/install_js.sh b/scripts/install_js.sh index 9444560..8cab474 100755 --- a/scripts/install_js.sh +++ b/scripts/install_js.sh @@ -6,7 +6,7 @@ if ! which npm &> /dev/null; then sudo apt-get install npm fi -npm --save-dev install \ +npm --prefix . --save-dev install \ qunit \ karma \ karma-qunit \ @@ -17,5 +17,5 @@ npm --save-dev install \ rollup-plugin-cleanup \ rollup-plugin-terser \ -npm --no-save install \ +npm --prefix . --no-save install \ https://github.com/jquery/jquery#main diff --git a/src/cone/maps/browser/map.py b/src/cone/maps/browser/map.py index 2c45b24..b4bf842 100644 --- a/src/cone/maps/browser/map.py +++ b/src/cone/maps/browser/map.py @@ -94,6 +94,20 @@ class MapTile(Tile): map_markers = [] """List of map markers to display. + + A map marker is represented by a dict like so: + + { + 'latlng': { + 'lat': 47.2688805, + 'lng': 11.3929127 + }, + 'options': { + 'title': 'Marker Tile' + } + } + + For available options see https://leafletjs.com/reference.html#marker """ map_markers_source = None @@ -132,8 +146,8 @@ def render(self): layers=json.dumps(self.map_layers), center=json.dumps(self.map_center), zoom=self.map_zoom, - markers=self.map_markers, + markers=json.dumps(self.map_markers), markers_source=self.map_markers_source, - marker_groups=self.map_marker_groups, + marker_groups=json.dumps(self.map_marker_groups), marker_groups_source=self.map_marker_groups_source, ) diff --git a/src/cone/maps/browser/static/maps/cone.maps.js b/src/cone/maps/browser/static/maps/cone.maps.js index 55aedff..6053bd9 100644 --- a/src/cone/maps/browser/static/maps/cone.maps.js +++ b/src/cone/maps/browser/static/maps/cone.maps.js @@ -1,4 +1,4 @@ -(function (exports, $) { +var cone_maps = (function (exports, $) { 'use strict'; function lookup_factory(name) { @@ -36,10 +36,10 @@ this.default_zoom = elem.data('map-zoom'); this.map_options = elem.data('map-options'); this.control_options = elem.data('map-control-options'); - this.markers = elem.data('data-map-markers'); - this.markers_source = elem.data('data-map-markers-source'); - this.marker_groups = elem.data('data-map-groups'); - this.marker_groups_source = elem.data('data-map-groups-source'); + this.markers = elem.data('map-markers'); + this.markers_source = elem.data('map-markers-source'); + this.marker_groups = elem.data('map-groups'); + this.marker_groups_source = elem.data('map-groups-source'); this.create(); elem.data('map-instance', this); } @@ -47,6 +47,7 @@ this.create_map(); this.create_controls(); this.create_layers(); + this.create_markers(); } create_map() { this.map = new L.Map(this.id, this.map_options); @@ -55,7 +56,7 @@ create_controls() { let base_maps = [], overlay_maps = []; - this.map_layers = new L.control.Layers( + this.map_layers = new L.Control.Layers( base_maps, overlay_maps, this.control_options @@ -84,6 +85,11 @@ remove_layer(layer) { this.map.removeLayer(layer); } + create_markers() { + for (let m of this.markers) { + new L.Marker(m.latlng, m.options).addTo(this.map); + } + } } $(function() { @@ -100,10 +106,6 @@ Object.defineProperty(exports, '__esModule', { value: true }); - - window.cone_maps = exports; - - return exports; })({}, jQuery); diff --git a/src/cone/maps/browser/static/maps/cone.maps.min.js b/src/cone/maps/browser/static/maps/cone.maps.min.js index 0215ea4..5ee8506 100644 --- a/src/cone/maps/browser/static/maps/cone.maps.min.js +++ b/src/cone/maps/browser/static/maps/cone.maps.min.js @@ -1 +1 @@ -!function(a,t){"use strict";function e(a){let t=window;for(let e of a.split("."))if(t=t[e],void 0===t)throw"Cannot locate map factory: "+a;return t}let r={tile_layer:function(a,t){a.layer_created(new L.TileLayer(t.urlTemplate,t.options),t)},geo_json:function(a,e){t.getJSON(e.dataUrl,(function(t){a.layer_created(new L.GeoJSON(t,e.options),e)}))}};class i{static initialize(a){t("div.cone-map",a).each((function(){let a=t(this);new(e(a.data("map-factory")))(a)}))}constructor(a){this.elem=a,this.id=a.attr("id"),this.layers=a.data("map-layers"),this.default_center=a.data("map-center"),this.default_zoom=a.data("map-zoom"),this.map_options=a.data("map-options"),this.control_options=a.data("map-control-options"),this.markers=a.data("data-map-markers"),this.markers_source=a.data("data-map-markers-source"),this.marker_groups=a.data("data-map-groups"),this.marker_groups_source=a.data("data-map-groups-source"),this.create(),a.data("map-instance",this)}create(){this.create_map(),this.create_controls(),this.create_layers()}create_map(){this.map=new L.Map(this.id,this.map_options),this.map.setView(this.default_center,this.default_zoom)}create_controls(){this.map_layers=new L.control.Layers([],[],this.control_options),this.map_layers.addTo(this.map)}create_layers(){for(let a of this.layers)r[a.factory](this,a)}layer_created(a,t){t.layer=a,(void 0===t.display||t.display)&&this.add_layer(a),"base"===t.category?this.map_layers.addBaseLayer(a,t.title):"overlay"===t.category&&this.map_layers.addOverlay(a,t.title)}add_layer(a){this.map.addLayer(a)}remove_layer(a){this.map.removeLayer(a)}}t((function(){void 0!==window.ts?ts.ajax.register(i.initialize,!0):bdajax.register(i.initialize,!0)})),a.Map=i,a.layer_factories=r,a.lookup_factory=e,Object.defineProperty(a,"__esModule",{value:!0}),window.cone_maps=a}({},jQuery); +var cone_maps=function(a,t){"use strict";function e(a){let t=window;for(let e of a.split("."))if(t=t[e],void 0===t)throw"Cannot locate map factory: "+a;return t}let r={tile_layer:function(a,t){a.layer_created(new L.TileLayer(t.urlTemplate,t.options),t)},geo_json:function(a,e){t.getJSON(e.dataUrl,(function(t){a.layer_created(new L.GeoJSON(t,e.options),e)}))}};class s{static initialize(a){t("div.cone-map",a).each((function(){let a=t(this);new(e(a.data("map-factory")))(a)}))}constructor(a){this.elem=a,this.id=a.attr("id"),this.layers=a.data("map-layers"),this.default_center=a.data("map-center"),this.default_zoom=a.data("map-zoom"),this.map_options=a.data("map-options"),this.control_options=a.data("map-control-options"),this.markers=a.data("map-markers"),this.markers_source=a.data("map-markers-source"),this.marker_groups=a.data("map-groups"),this.marker_groups_source=a.data("map-groups-source"),this.create(),a.data("map-instance",this)}create(){this.create_map(),this.create_controls(),this.create_layers(),this.create_markers()}create_map(){this.map=new L.Map(this.id,this.map_options),this.map.setView(this.default_center,this.default_zoom)}create_controls(){this.map_layers=new L.Control.Layers([],[],this.control_options),this.map_layers.addTo(this.map)}create_layers(){for(let a of this.layers)r[a.factory](this,a)}layer_created(a,t){t.layer=a,(void 0===t.display||t.display)&&this.add_layer(a),"base"===t.category?this.map_layers.addBaseLayer(a,t.title):"overlay"===t.category&&this.map_layers.addOverlay(a,t.title)}add_layer(a){this.map.addLayer(a)}remove_layer(a){this.map.removeLayer(a)}create_markers(){for(let a of this.markers)new L.Marker(a.latlng,a.options).addTo(this.map)}}return t((function(){void 0!==window.ts?ts.ajax.register(s.initialize,!0):bdajax.register(s.initialize,!0)})),a.Map=s,a.layer_factories=r,a.lookup_factory=e,Object.defineProperty(a,"__esModule",{value:!0}),a}({},jQuery); From c721e72695ffd2d4d8d5da06800e7569b8b1c041 Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Wed, 27 Jul 2022 14:19:56 +0200 Subject: [PATCH 06/29] Implement map markers from JSON source. --- CHANGES.rst | 3 ++ js/src/map.js | 24 ++++++++++++-- src/cone/maps/browser/map.py | 32 +++++++++++++------ .../maps/browser/static/maps/cone.maps.js | 23 +++++++++++-- .../maps/browser/static/maps/cone.maps.min.js | 2 +- 5 files changed, 69 insertions(+), 15 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 931031f..dd657b4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,9 @@ Changes - Fix control layers creation in ``Map.create_controls``. [rnix] +- Implement map markers from JSON source. + [rnix] + - Implement static map markers. [rnix] diff --git a/js/src/map.js b/js/src/map.js index 6e1f235..dc3ab24 100644 --- a/js/src/map.js +++ b/js/src/map.js @@ -102,8 +102,28 @@ export class Map { } create_markers() { - for (let m of this.markers) { - new L.Marker(m.latlng, m.options).addTo(this.map); + for (let marker of this.markers) { + this.create_marker(marker); + } + if (this.markers_source) { + $.getJSON(this.markers_source, function(data) { + for (let marker of data) { + this.create_marker(marker); + } + }.bind(this)); + } + if (this.markers || this.markers_source) { + this.map.on('popupopen', function(evt) { + let popup = evt.popup; + ts.ajax.bind($(popup._contentNode)); + }); + } + } + + create_marker(marker) { + let m = new L.Marker(marker.latlng, marker.options).addTo(this.map); + if (marker.popup) { + m.bindPopup(marker.popup.content, marker.popup.options); } } } diff --git a/src/cone/maps/browser/map.py b/src/cone/maps/browser/map.py index b4bf842..3a82e12 100644 --- a/src/cone/maps/browser/map.py +++ b/src/cone/maps/browser/map.py @@ -39,8 +39,7 @@ class MapTile(Tile): """ map_id = 'map' - """HTML id of the map. - """ + """HTML id of the map.""" map_css = 'cone-map' """CSS class of the map element. @@ -85,12 +84,10 @@ class MapTile(Tile): """ map_center = [47.2688805, 11.3929127] - """The default (initial) center of the map as lat/lng. - """ + """The default (initial) center of the map as lat/lng.""" map_zoom = 8 - """The default (initial) zoom level of the map - """ + """The default (initial) zoom level of the map.""" map_markers = [] """List of map markers to display. @@ -104,22 +101,37 @@ class MapTile(Tile): }, 'options': { 'title': 'Marker Tile' + }, + 'popup': { + 'content': '
Marker Popup
', + 'options': { + 'keepInView': True + } } } - For available options see https://leafletjs.com/reference.html#marker + For available marker options, + see https://leafletjs.com/reference.html#marker + + For available popup options, + see https://leafletjs.com/reference.html#popup-option """ map_markers_source = None - """JSON endpoint to fetch markers from. + """JSON endpoint to fetch markers from. For details about the expected + format, see ``map_markers``. """ map_marker_groups = [] """List of map marker groups to display. + + Not implemented in JS yet. """ map_marker_groups_source = None """JSON endpoint to fetch marker groups from. + + Not implemented in JS yet. """ def render(self): @@ -147,7 +159,7 @@ def render(self): center=json.dumps(self.map_center), zoom=self.map_zoom, markers=json.dumps(self.map_markers), - markers_source=self.map_markers_source, + markers_source=self.map_markers_source or '', marker_groups=json.dumps(self.map_marker_groups), - marker_groups_source=self.map_marker_groups_source, + marker_groups_source=self.map_marker_groups_source or '' ) diff --git a/src/cone/maps/browser/static/maps/cone.maps.js b/src/cone/maps/browser/static/maps/cone.maps.js index 6053bd9..27498f7 100644 --- a/src/cone/maps/browser/static/maps/cone.maps.js +++ b/src/cone/maps/browser/static/maps/cone.maps.js @@ -86,8 +86,27 @@ var cone_maps = (function (exports, $) { this.map.removeLayer(layer); } create_markers() { - for (let m of this.markers) { - new L.Marker(m.latlng, m.options).addTo(this.map); + for (let marker of this.markers) { + this.create_marker(marker); + } + if (this.markers_source) { + $.getJSON(this.markers_source, function(data) { + for (let marker of data) { + this.create_marker(marker); + } + }.bind(this)); + } + if (this.markers || this.markers_source) { + this.map.on('popupopen', function(evt) { + let popup = evt.popup; + ts.ajax.bind($(popup._contentNode)); + }); + } + } + create_marker(marker) { + let m = new L.Marker(marker.latlng, marker.options).addTo(this.map); + if (marker.popup) { + m.bindPopup(marker.popup.content, marker.popup.options); } } } diff --git a/src/cone/maps/browser/static/maps/cone.maps.min.js b/src/cone/maps/browser/static/maps/cone.maps.min.js index 5ee8506..b0ffe11 100644 --- a/src/cone/maps/browser/static/maps/cone.maps.min.js +++ b/src/cone/maps/browser/static/maps/cone.maps.min.js @@ -1 +1 @@ -var cone_maps=function(a,t){"use strict";function e(a){let t=window;for(let e of a.split("."))if(t=t[e],void 0===t)throw"Cannot locate map factory: "+a;return t}let r={tile_layer:function(a,t){a.layer_created(new L.TileLayer(t.urlTemplate,t.options),t)},geo_json:function(a,e){t.getJSON(e.dataUrl,(function(t){a.layer_created(new L.GeoJSON(t,e.options),e)}))}};class s{static initialize(a){t("div.cone-map",a).each((function(){let a=t(this);new(e(a.data("map-factory")))(a)}))}constructor(a){this.elem=a,this.id=a.attr("id"),this.layers=a.data("map-layers"),this.default_center=a.data("map-center"),this.default_zoom=a.data("map-zoom"),this.map_options=a.data("map-options"),this.control_options=a.data("map-control-options"),this.markers=a.data("map-markers"),this.markers_source=a.data("map-markers-source"),this.marker_groups=a.data("map-groups"),this.marker_groups_source=a.data("map-groups-source"),this.create(),a.data("map-instance",this)}create(){this.create_map(),this.create_controls(),this.create_layers(),this.create_markers()}create_map(){this.map=new L.Map(this.id,this.map_options),this.map.setView(this.default_center,this.default_zoom)}create_controls(){this.map_layers=new L.Control.Layers([],[],this.control_options),this.map_layers.addTo(this.map)}create_layers(){for(let a of this.layers)r[a.factory](this,a)}layer_created(a,t){t.layer=a,(void 0===t.display||t.display)&&this.add_layer(a),"base"===t.category?this.map_layers.addBaseLayer(a,t.title):"overlay"===t.category&&this.map_layers.addOverlay(a,t.title)}add_layer(a){this.map.addLayer(a)}remove_layer(a){this.map.removeLayer(a)}create_markers(){for(let a of this.markers)new L.Marker(a.latlng,a.options).addTo(this.map)}}return t((function(){void 0!==window.ts?ts.ajax.register(s.initialize,!0):bdajax.register(s.initialize,!0)})),a.Map=s,a.layer_factories=r,a.lookup_factory=e,Object.defineProperty(a,"__esModule",{value:!0}),a}({},jQuery); +var cone_maps=function(t,e){"use strict";function a(t){let e=window;for(let a of t.split("."))if(e=e[a],void 0===e)throw"Cannot locate map factory: "+t;return e}let r={tile_layer:function(t,e){t.layer_created(new L.TileLayer(e.urlTemplate,e.options),e)},geo_json:function(t,a){e.getJSON(a.dataUrl,(function(e){t.layer_created(new L.GeoJSON(e,a.options),a)}))}};class s{static initialize(t){e("div.cone-map",t).each((function(){let t=e(this);new(a(t.data("map-factory")))(t)}))}constructor(t){this.elem=t,this.id=t.attr("id"),this.layers=t.data("map-layers"),this.default_center=t.data("map-center"),this.default_zoom=t.data("map-zoom"),this.map_options=t.data("map-options"),this.control_options=t.data("map-control-options"),this.markers=t.data("map-markers"),this.markers_source=t.data("map-markers-source"),this.marker_groups=t.data("map-groups"),this.marker_groups_source=t.data("map-groups-source"),this.create(),t.data("map-instance",this)}create(){this.create_map(),this.create_controls(),this.create_layers(),this.create_markers()}create_map(){this.map=new L.Map(this.id,this.map_options),this.map.setView(this.default_center,this.default_zoom)}create_controls(){this.map_layers=new L.Control.Layers([],[],this.control_options),this.map_layers.addTo(this.map)}create_layers(){for(let t of this.layers)r[t.factory](this,t)}layer_created(t,e){e.layer=t,(void 0===e.display||e.display)&&this.add_layer(t),"base"===e.category?this.map_layers.addBaseLayer(t,e.title):"overlay"===e.category&&this.map_layers.addOverlay(t,e.title)}add_layer(t){this.map.addLayer(t)}remove_layer(t){this.map.removeLayer(t)}create_markers(){for(let t of this.markers)this.create_marker(t);this.markers_source&&e.getJSON(this.markers_source,function(t){for(let e of t)this.create_marker(e)}.bind(this)),(this.markers||this.markers_source)&&this.map.on("popupopen",(function(t){let a=t.popup;ts.ajax.bind(e(a._contentNode))}))}create_marker(t){let e=new L.Marker(t.latlng,t.options).addTo(this.map);t.popup&&e.bindPopup(t.popup.content,t.popup.options)}}return e((function(){void 0!==window.ts?ts.ajax.register(s.initialize,!0):bdajax.register(s.initialize,!0)})),t.Map=s,t.layer_factories=r,t.lookup_factory=a,Object.defineProperty(t,"__esModule",{value:!0}),t}({},jQuery); From 12e227ebcf044fd94cff6f894acc1261553743c6 Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Mon, 21 Nov 2022 10:19:38 +0100 Subject: [PATCH 07/29] Add ``MapTile.map_bounds`` property. Map settings gets rendered as single data attribute. --- CHANGES.rst | 6 +++ js/src/map.js | 46 +++++++++---------- src/cone/maps/browser/map.py | 43 +++++++++-------- .../maps/browser/static/maps/cone.maps.js | 46 +++++++++---------- .../maps/browser/static/maps/cone.maps.min.js | 2 +- 5 files changed, 70 insertions(+), 73 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index dd657b4..54409af 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,12 @@ Changes 0.2 (unreleased) ---------------- +- Map settings gets rendered as single data attribute. + [rnix] + +- Add ``MapTile.map_bounds`` property. + [rnix] + - Fix control layers creation in ``Map.create_controls``. [rnix] diff --git a/js/src/map.js b/js/src/map.js index dc3ab24..1690a49 100644 --- a/js/src/map.js +++ b/js/src/map.js @@ -1,16 +1,5 @@ import $ from 'jquery'; -export function lookup_factory(name) { - let ob = window; - for (let part of name.split('.')) { - ob = ob[part]; - if (ob === undefined) { - throw "Cannot locate map factory: " + name; - } - } - return ob; -} - let layer_factories = {}; export {layer_factories}; @@ -28,25 +17,28 @@ export class Map { static initialize(context) { $('div.cone-map', context).each(function() { - let elem = $(this); - let factory = lookup_factory(elem.data('map-factory')); - new factory(elem); + let elem = $(this), + settings = elem.data('map-settings'), + factory_path = settings.factory, + factory = ts.object_by_path(factory_path); + new factory(elem, settings); }); } - constructor(elem) { + constructor(elem, settings) { this.elem = elem; this.id = elem.attr('id'); - this.layers = elem.data('map-layers'); - this.default_center = elem.data('map-center'); - this.default_zoom = elem.data('map-zoom'); - this.map_options = elem.data('map-options'); - this.control_options = elem.data('map-control-options'); - this.markers = elem.data('map-markers'); - this.markers_source = elem.data('map-markers-source'); - this.marker_groups = elem.data('map-groups'); - this.marker_groups_source = elem.data('map-groups-source'); + this.layers = settings.layers + this.default_center = settings.center + this.default_zoom = settings.zoom + this.default_bounds = settings.bounds + this.map_options = settings.options + this.control_options = settings.control_options + this.markers = settings.markers + this.markers_source = settings.markers_source + this.marker_groups = settings.groups + this.marker_groups_source = settings.groups_source this.create(); elem.data('map-instance', this); @@ -61,7 +53,11 @@ export class Map { create_map() { this.map = new L.Map(this.id, this.map_options); - this.map.setView(this.default_center, this.default_zoom); + if (this.default_bounds.length) { + this.map.fitBounds(this.default_bounds); + } else { + this.map.setView(this.default_center, this.default_zoom); + } } create_controls() { diff --git a/src/cone/maps/browser/map.py b/src/cone/maps/browser/map.py index 3a82e12..f1df9b7 100644 --- a/src/cone/maps/browser/map.py +++ b/src/cone/maps/browser/map.py @@ -89,6 +89,10 @@ class MapTile(Tile): map_zoom = 8 """The default (initial) zoom level of the map.""" + map_bounds = [] + """A list of geo points. If set, bounds take precedence over ``map_center`` + and ``map_zoom`` and the map gets positioned to fit the bounds.""" + map_markers = [] """List of map markers to display. @@ -117,7 +121,7 @@ class MapTile(Tile): see https://leafletjs.com/reference.html#popup-option """ - map_markers_source = None + map_markers_source = '' """JSON endpoint to fetch markers from. For details about the expected format, see ``map_markers``. """ @@ -128,38 +132,33 @@ class MapTile(Tile): Not implemented in JS yet. """ - map_marker_groups_source = None + map_marker_groups_source = '' """JSON endpoint to fetch marker groups from. Not implemented in JS yet. """ def render(self): + settings = dict( + factory=self.map_factory, + options=self.map_options, + control_options=self.map_control_options, + layers=self.map_layers, + center=self.map_center, + zoom=self.map_zoom, + bounds=self.map_bounds, + markers=self.map_markers, + markers_source=self.map_markers_source, + groups=self.map_marker_groups, + groups_source=self.map_marker_groups_source + ) return ( u'
' + u' data-map-settings=\'{settings}\' >' u'
' ).format( css=self.map_css, id=self.map_id, - factory=self.map_factory, - options=json.dumps(self.map_options), - control_options=json.dumps(self.map_control_options), - layers=json.dumps(self.map_layers), - center=json.dumps(self.map_center), - zoom=self.map_zoom, - markers=json.dumps(self.map_markers), - markers_source=self.map_markers_source or '', - marker_groups=json.dumps(self.map_marker_groups), - marker_groups_source=self.map_marker_groups_source or '' + settings=json.dumps(settings) ) diff --git a/src/cone/maps/browser/static/maps/cone.maps.js b/src/cone/maps/browser/static/maps/cone.maps.js index 27498f7..0571f3a 100644 --- a/src/cone/maps/browser/static/maps/cone.maps.js +++ b/src/cone/maps/browser/static/maps/cone.maps.js @@ -1,16 +1,6 @@ var cone_maps = (function (exports, $) { 'use strict'; - function lookup_factory(name) { - let ob = window; - for (let part of name.split('.')) { - ob = ob[part]; - if (ob === undefined) { - throw "Cannot locate map factory: " + name; - } - } - return ob; - } let layer_factories = {}; layer_factories.tile_layer = function(inst, cfg) { inst.layer_created(new L.TileLayer(cfg.urlTemplate, cfg.options), cfg); @@ -23,23 +13,26 @@ var cone_maps = (function (exports, $) { class Map { static initialize(context) { $('div.cone-map', context).each(function() { - let elem = $(this); - let factory = lookup_factory(elem.data('map-factory')); - new factory(elem); + let elem = $(this), + settings = elem.data('map-settings'), + factory_path = settings.factory, + factory = ts.object_by_path(factory_path); + new factory(elem, settings); }); } - constructor(elem) { + constructor(elem, settings) { this.elem = elem; this.id = elem.attr('id'); - this.layers = elem.data('map-layers'); - this.default_center = elem.data('map-center'); - this.default_zoom = elem.data('map-zoom'); - this.map_options = elem.data('map-options'); - this.control_options = elem.data('map-control-options'); - this.markers = elem.data('map-markers'); - this.markers_source = elem.data('map-markers-source'); - this.marker_groups = elem.data('map-groups'); - this.marker_groups_source = elem.data('map-groups-source'); + this.layers = settings.layers; + this.default_center = settings.center; + this.default_zoom = settings.zoom; + this.default_bounds = settings.bounds; + this.map_options = settings.options; + this.control_options = settings.control_options; + this.markers = settings.markers; + this.markers_source = settings.markers_source; + this.marker_groups = settings.groups; + this.marker_groups_source = settings.groups_source; this.create(); elem.data('map-instance', this); } @@ -51,7 +44,11 @@ var cone_maps = (function (exports, $) { } create_map() { this.map = new L.Map(this.id, this.map_options); - this.map.setView(this.default_center, this.default_zoom); + if (this.default_bounds.length) { + this.map.fitBounds(this.default_bounds); + } else { + this.map.setView(this.default_center, this.default_zoom); + } } create_controls() { let base_maps = [], @@ -121,7 +118,6 @@ var cone_maps = (function (exports, $) { exports.Map = Map; exports.layer_factories = layer_factories; - exports.lookup_factory = lookup_factory; Object.defineProperty(exports, '__esModule', { value: true }); diff --git a/src/cone/maps/browser/static/maps/cone.maps.min.js b/src/cone/maps/browser/static/maps/cone.maps.min.js index b0ffe11..be22490 100644 --- a/src/cone/maps/browser/static/maps/cone.maps.min.js +++ b/src/cone/maps/browser/static/maps/cone.maps.min.js @@ -1 +1 @@ -var cone_maps=function(t,e){"use strict";function a(t){let e=window;for(let a of t.split("."))if(e=e[a],void 0===e)throw"Cannot locate map factory: "+t;return e}let r={tile_layer:function(t,e){t.layer_created(new L.TileLayer(e.urlTemplate,e.options),e)},geo_json:function(t,a){e.getJSON(a.dataUrl,(function(e){t.layer_created(new L.GeoJSON(e,a.options),a)}))}};class s{static initialize(t){e("div.cone-map",t).each((function(){let t=e(this);new(a(t.data("map-factory")))(t)}))}constructor(t){this.elem=t,this.id=t.attr("id"),this.layers=t.data("map-layers"),this.default_center=t.data("map-center"),this.default_zoom=t.data("map-zoom"),this.map_options=t.data("map-options"),this.control_options=t.data("map-control-options"),this.markers=t.data("map-markers"),this.markers_source=t.data("map-markers-source"),this.marker_groups=t.data("map-groups"),this.marker_groups_source=t.data("map-groups-source"),this.create(),t.data("map-instance",this)}create(){this.create_map(),this.create_controls(),this.create_layers(),this.create_markers()}create_map(){this.map=new L.Map(this.id,this.map_options),this.map.setView(this.default_center,this.default_zoom)}create_controls(){this.map_layers=new L.Control.Layers([],[],this.control_options),this.map_layers.addTo(this.map)}create_layers(){for(let t of this.layers)r[t.factory](this,t)}layer_created(t,e){e.layer=t,(void 0===e.display||e.display)&&this.add_layer(t),"base"===e.category?this.map_layers.addBaseLayer(t,e.title):"overlay"===e.category&&this.map_layers.addOverlay(t,e.title)}add_layer(t){this.map.addLayer(t)}remove_layer(t){this.map.removeLayer(t)}create_markers(){for(let t of this.markers)this.create_marker(t);this.markers_source&&e.getJSON(this.markers_source,function(t){for(let e of t)this.create_marker(e)}.bind(this)),(this.markers||this.markers_source)&&this.map.on("popupopen",(function(t){let a=t.popup;ts.ajax.bind(e(a._contentNode))}))}create_marker(t){let e=new L.Marker(t.latlng,t.options).addTo(this.map);t.popup&&e.bindPopup(t.popup.content,t.popup.options)}}return e((function(){void 0!==window.ts?ts.ajax.register(s.initialize,!0):bdajax.register(s.initialize,!0)})),t.Map=s,t.layer_factories=r,t.lookup_factory=a,Object.defineProperty(t,"__esModule",{value:!0}),t}({},jQuery); +var cone_maps=function(e,t){"use strict";let a={tile_layer:function(e,t){e.layer_created(new L.TileLayer(t.urlTemplate,t.options),t)},geo_json:function(e,a){t.getJSON(a.dataUrl,(function(t){e.layer_created(new L.GeoJSON(t,a.options),a)}))}};class s{static initialize(e){t("div.cone-map",e).each((function(){let e=t(this),a=e.data("map-settings"),s=a.factory;new(ts.object_by_path(s))(e,a)}))}constructor(e,t){this.elem=e,this.id=e.attr("id"),this.layers=t.layers,this.default_center=t.center,this.default_zoom=t.zoom,this.default_bounds=t.bounds,this.map_options=t.options,this.control_options=t.control_options,this.markers=t.markers,this.markers_source=t.markers_source,this.marker_groups=t.groups,this.marker_groups_source=t.groups_source,this.create(),e.data("map-instance",this)}create(){this.create_map(),this.create_controls(),this.create_layers(),this.create_markers()}create_map(){this.map=new L.Map(this.id,this.map_options),this.default_bounds.length?this.map.fitBounds(this.default_bounds):this.map.setView(this.default_center,this.default_zoom)}create_controls(){this.map_layers=new L.Control.Layers([],[],this.control_options),this.map_layers.addTo(this.map)}create_layers(){for(let e of this.layers)a[e.factory](this,e)}layer_created(e,t){t.layer=e,(void 0===t.display||t.display)&&this.add_layer(e),"base"===t.category?this.map_layers.addBaseLayer(e,t.title):"overlay"===t.category&&this.map_layers.addOverlay(e,t.title)}add_layer(e){this.map.addLayer(e)}remove_layer(e){this.map.removeLayer(e)}create_markers(){for(let e of this.markers)this.create_marker(e);this.markers_source&&t.getJSON(this.markers_source,function(e){for(let t of e)this.create_marker(t)}.bind(this)),(this.markers||this.markers_source)&&this.map.on("popupopen",(function(e){let a=e.popup;ts.ajax.bind(t(a._contentNode))}))}create_marker(e){let t=new L.Marker(e.latlng,e.options).addTo(this.map);e.popup&&t.bindPopup(e.popup.content,e.popup.options)}}return t((function(){void 0!==window.ts?ts.ajax.register(s.initialize,!0):bdajax.register(s.initialize,!0)})),e.Map=s,e.layer_factories=a,Object.defineProperty(e,"__esModule",{value:!0}),e}({},jQuery); From ba53441998a592f964609de104c237b6172956f5 Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Mon, 21 Nov 2022 12:11:31 +0100 Subject: [PATCH 08/29] Fix tests --- src/cone/maps/tests.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/cone/maps/tests.py b/src/cone/maps/tests.py index 1aee947..44ab01c 100644 --- a/src/cone/maps/tests.py +++ b/src/cone/maps/tests.py @@ -32,16 +32,18 @@ def test_map_tile(self): self.assertTrue(res.find('class="cone-map"') > -1) self.assertTrue(res.find('id="map"') > -1) - self.assertTrue(res.find('data-map-factory="cone_maps.Map"') > -1) - self.assertTrue(res.find('data-map-options=') > -1) - self.assertTrue(res.find('data-map-control-options=') > -1) - self.assertTrue(res.find('data-map-layers=') > -1) - self.assertTrue(res.find('data-map-center=') > -1) - self.assertTrue(res.find('data-map-zoom="8"') > -1) - self.assertTrue(res.find('data-map-markers=') > -1) - self.assertTrue(res.find('data-map-markers-source=') > -1) - self.assertTrue(res.find('data-map-groups=') > -1) - self.assertTrue(res.find('data-map-groups-source=') > -1) + self.assertTrue(res.find('data-map-settings=') > -1) + self.assertTrue(res.find('"factory": "cone_maps.Map"') > -1) + self.assertTrue(res.find('"options": {') > -1) + self.assertTrue(res.find('"control_options": {') > -1) + self.assertTrue(res.find('"layers": [') > -1) + self.assertTrue(res.find('"center": [') > -1) + self.assertTrue(res.find('"zoom": 8') > -1) + self.assertTrue(res.find('"bounds": []') > -1) + self.assertTrue(res.find('"markers": []') > -1) + self.assertTrue(res.find('"markers_source": ""') > -1) + self.assertTrue(res.find('"groups": []') > -1) + self.assertTrue(res.find('"groups_source": ""') > -1) def np(path): From 4a84b0cfff6188397992507421f542c09938fa3b Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Mon, 21 Nov 2022 12:29:31 +0100 Subject: [PATCH 09/29] Map settings are defined via ``MapTile.map_settings`` property. --- CHANGES.rst | 3 +++ src/cone/maps/browser/map.py | 35 ++++++++++++++++++++++------------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 54409af..f3b3999 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,9 @@ Changes 0.2 (unreleased) ---------------- +- Map settings are defined via ``MapTile.map_settings`` property. + [rnix] + - Map settings gets rendered as single data attribute. [rnix] diff --git a/src/cone/maps/browser/map.py b/src/cone/maps/browser/map.py index f1df9b7..42318c6 100644 --- a/src/cone/maps/browser/map.py +++ b/src/cone/maps/browser/map.py @@ -11,29 +11,29 @@ class MapTile(Tile): map_factory = 'cone_maps.Map' """Factory used for map creation in Javascript. - The definded factory must accept the map related DOM element as argument - and is responsible to initialize the leaflet map. + The definded factory must accept the map related DOM element and the map + settings as arguments and is responsible to initialize the leaflet map. - It points to a class or function and does property lookup on window with - '.' as delimiter, e.g: + It points to a class or function and gets searched by dot separated path + on window, e.g: 'cone_maps.Map' corresponds to: - cone_maps: { - Map: {} + window.cone_maps: { + Map: ... } - If JS map factory needs to be customized, this is usually done by subclassing - 'cone_maps.Map': + If JS map factory needs to be customized, this is usually done by + subclassing 'cone_maps.Map': my_namespace = {}; my_namespace.Map = class extends cone_maps.Map { - constructor(elem) { + constructor(elem, settings) { elem.height(800); - super(elem); + super(elem, settings); } } """ @@ -138,8 +138,15 @@ class MapTile(Tile): Not implemented in JS yet. """ - def render(self): - settings = dict( + @property + def map_settings(self): + """Dictionary containing map settings. This settings get passed to + the JS map constructor. + + This property can be customized to pass additional settings to + custom map factory if needed. + """ + return dict( factory=self.map_factory, options=self.map_options, control_options=self.map_control_options, @@ -152,6 +159,8 @@ def render(self): groups=self.map_marker_groups, groups_source=self.map_marker_groups_source ) + + def render(self): return ( u'
Date: Sat, 10 Dec 2022 09:21:39 +0100 Subject: [PATCH 10/29] use cone.app resource registry --- src/cone/maps/__init__.py | 2 +- src/cone/maps/browser/__init__.py | 60 +++++++++++++++---------------- src/cone/maps/tests.py | 28 ++++++++++----- 3 files changed, 50 insertions(+), 40 deletions(-) diff --git a/src/cone/maps/__init__.py b/src/cone/maps/__init__.py index fd02355..ba65c3b 100644 --- a/src/cone/maps/__init__.py +++ b/src/cone/maps/__init__.py @@ -11,7 +11,7 @@ def initialize_maps(config, global_config, settings): # application startup initialization # static resources - configure_resources(settings) + configure_resources(config, settings) # add translation config.add_translation_dirs('cone.maps:locale/') diff --git a/src/cone/maps/browser/__init__.py b/src/cone/maps/browser/__init__.py index 4f438ae..bb70dcd 100644 --- a/src/cone/maps/browser/__init__.py +++ b/src/cone/maps/browser/__init__.py @@ -1,5 +1,3 @@ -from cone.app.browser.resources import resources -from cone.app.browser.resources import set_resource_include import webresource as wr import os @@ -11,8 +9,7 @@ leaflet_resources = wr.ResourceGroup( name='cone.maps-leaflet', directory=os.path.join(resources_dir, 'leaflet'), - path='leaflet', - group=resources + path='leaflet' ) leaflet_resources.add(wr.ScriptResource( name='leaflet-js', @@ -28,8 +25,7 @@ leaflet_nogap_resources = wr.ResourceGroup( name='cone.maps-leaflet-nogap', directory=os.path.join(resources_dir, 'leaflet-nogap'), - path='leaflet-nogap', - group=resources + path='leaflet-nogap' ) leaflet_nogap_resources.add(wr.ScriptResource( name='leaflet-nogap-js', @@ -41,8 +37,7 @@ leaflet_geosearch_resources = wr.ResourceGroup( name='cone.maps-leaflet-geosearch', directory=os.path.join(resources_dir, 'leaflet-geosearch'), - path='leaflet-geosearch', - group=resources + path='leaflet-geosearch' ) leaflet_geosearch_resources.add(wr.ScriptResource( name='leaflet-geosearch-js', @@ -59,8 +54,7 @@ leaflet_markercluster_resources = wr.ResourceGroup( name='cone.maps-leaflet-markercluster', directory=os.path.join(resources_dir, 'leaflet-markercluster'), - path='leaflet-markercluster', - group=resources + path='leaflet-markercluster' ) leaflet_markercluster_resources.add(wr.ScriptResource( name='leaflet-markercluster-js', @@ -83,8 +77,7 @@ leaflet_activearea_resources = wr.ResourceGroup( name='cone.maps-leaflet-activearea', directory=os.path.join(resources_dir, 'leaflet-activearea'), - path='leaflet-activearea', - group=resources + path='leaflet-activearea' ) leaflet_activearea_resources.add(wr.ScriptResource( name='leaflet-activearea-js', @@ -96,8 +89,7 @@ proj4_resources = wr.ResourceGroup( name='cone.maps-proj4', directory=os.path.join(resources_dir, 'proj4js'), - path='proj4js', - group=resources + path='proj4js' ) proj4_resources.add(wr.ScriptResource( name='proj4-js', @@ -109,8 +101,7 @@ leaflet_proj4_resources = wr.ResourceGroup( name='cone.maps-leaflet-proj4', directory=os.path.join(resources_dir, 'leaflet-proj4'), - path='leaflet-proj4', - group=resources + path='leaflet-proj4' ) leaflet_proj4_resources.add(wr.ScriptResource( name='leaflet-proj4-js', @@ -122,8 +113,7 @@ cone_maps_resources = wr.ResourceGroup( name='cone.maps-maps', directory=os.path.join(resources_dir, 'maps'), - path='maps', - group=resources + path='maps' ) cone_maps_resources.add(wr.ScriptResource( name='cone-maps-js', @@ -133,39 +123,47 @@ )) -def configure_resources(settings): +def configure_resources(config, settings): def included(name): return settings.get(name, 'false') == 'true' include = True if included('cone.maps.public') else 'authenticated' # leaflet core - set_resource_include(settings, 'leaflet-js', include) - set_resource_include(settings, 'leaflet-css', include) + config.register_resource(leaflet_resources) + config.set_resource_include('leaflet-js', include) + config.set_resource_include('leaflet-css', include) # Leaflet.TileLayer.NoGap + config.register_resource(leaflet_nogap_resources) nogap_include = include if included('cone.maps.nogap') else False - set_resource_include(settings, 'leaflet-nogap-js', nogap_include) + config.set_resource_include('leaflet-nogap-js', nogap_include) # leaflet-geosearch + config.register_resource(leaflet_geosearch_resources) geosearch_include = include if included('cone.maps.geosearch') else False - set_resource_include(settings, 'leaflet-geosearch-js', geosearch_include) - set_resource_include(settings, 'leaflet-geosearch-css', geosearch_include) + config.set_resource_include('leaflet-geosearch-js', geosearch_include) + config.set_resource_include('leaflet-geosearch-css', geosearch_include) # Leaflet.markercluster + config.register_resource(leaflet_markercluster_resources) mc_include = include if included('cone.maps.markercluster') else False - set_resource_include(settings, 'leaflet-markercluster-js', mc_include) - set_resource_include(settings, 'leaflet-markercluster-css', mc_include) - set_resource_include(settings, 'leaflet-markercluster-default-css', mc_include) + config.set_resource_include('leaflet-markercluster-js', mc_include) + config.set_resource_include('leaflet-markercluster-css', mc_include) + config.set_resource_include('leaflet-markercluster-default-css', mc_include) # Leaflet-active-area + config.register_resource(leaflet_activearea_resources) activearea_include = include if included('cone.maps.activearea') else False - set_resource_include(settings, 'leaflet-activearea-js', activearea_include) + config.set_resource_include('leaflet-activearea-js', activearea_include) # proj4js and Proj4Leaflet + config.register_resource(proj4_resources) + config.register_resource(leaflet_proj4_resources) proj4_include = include if included('cone.maps.proj4') else False - set_resource_include(settings, 'proj4-js', proj4_include) - set_resource_include(settings, 'leaflet-proj4-js', proj4_include) + config.set_resource_include('proj4-js', proj4_include) + config.set_resource_include('leaflet-proj4-js', proj4_include) # cone maps - set_resource_include(settings, 'cone-maps-js', include) + config.register_resource(cone_maps_resources) + config.set_resource_include('cone-maps-js', include) diff --git a/src/cone/maps/tests.py b/src/cone/maps/tests.py index 44ab01c..34d2a39 100644 --- a/src/cone/maps/tests.py +++ b/src/cone/maps/tests.py @@ -1,5 +1,5 @@ from cone.app import testing -from cone.app.browser.resources import RESOURCE_INCLUDES_KEY +from cone.app.browser import resources from cone.app.model import BaseNode from cone.maps import browser from cone.maps.browser.map import MapTile @@ -210,13 +210,23 @@ def test_cone_maps_resources(self): self.assertEqual(len(styles), 0) def test_configure_resources(self): - configure_resources = browser.configure_resources + class TestConfigurator: + def __init__(self): + self.includes = {} + + def register_resource(self, resource): + pass + def set_resource_include(self, name, value): + self.includes[name] = value + + configure_resources = browser.configure_resources + config = TestConfigurator() settings = { 'cone.maps.public': 'false' } - configure_resources(settings) - self.assertEqual(settings[RESOURCE_INCLUDES_KEY], { + configure_resources(config, settings) + self.assertEqual(config.includes, { 'leaflet-js': 'authenticated', 'leaflet-css': 'authenticated', 'leaflet-nogap-js': False, @@ -231,6 +241,7 @@ def test_configure_resources(self): 'cone-maps-js': 'authenticated' }) + config = TestConfigurator() settings = { 'cone.maps.public': 'false', 'cone.maps.nogap': 'true', @@ -239,8 +250,8 @@ def test_configure_resources(self): 'cone.maps.activearea': 'true', 'cone.maps.proj4': 'true' } - configure_resources(settings) - self.assertEqual(settings[RESOURCE_INCLUDES_KEY], { + configure_resources(config, settings) + self.assertEqual(config.includes, { 'leaflet-js': 'authenticated', 'leaflet-css': 'authenticated', 'leaflet-nogap-js': 'authenticated', @@ -255,6 +266,7 @@ def test_configure_resources(self): 'cone-maps-js': 'authenticated' }) + config = TestConfigurator() settings = { 'cone.maps.public': 'true', 'cone.maps.nogap': 'true', @@ -263,8 +275,8 @@ def test_configure_resources(self): 'cone.maps.activearea': 'true', 'cone.maps.proj4': 'true' } - configure_resources(settings) - self.assertEqual(settings[RESOURCE_INCLUDES_KEY], { + configure_resources(config, settings) + self.assertEqual(config.includes, { 'leaflet-js': True, 'leaflet-css': True, 'leaflet-nogap-js': True, From a96549663bc47358e044c13ac4c7a1aac0928435 Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Sun, 11 Dec 2022 14:50:33 +0100 Subject: [PATCH 11/29] Add Leaflet.Editable --- README.rst | 7 + src/cone/maps/browser/__init__.py | 17 + .../leaflet-editable/Leaflet.Editable.js | 1946 +++++++++++++++++ 3 files changed, 1970 insertions(+) create mode 100644 src/cone/maps/browser/static/leaflet-editable/Leaflet.Editable.js diff --git a/README.rst b/README.rst index b37ee7d..df1d8f5 100644 --- a/README.rst +++ b/README.rst @@ -30,6 +30,10 @@ This package provides maps integration in to cone.app. `Leaflet.markercluster `_ (1.5.3) is included. +* Make meking geometries editable in Leaflet, + `Leaflet.Editable `_ + (1.2.0) is included. + * For defining active map area, e.g. if parts of a map is used as background, `Leaflet-active-area `_ (1.2.0) is included. @@ -79,6 +83,9 @@ available : - **cone.maps.markercluster**: Flag whether to include ``Leaflet.markercluster`` plugin. Defaults to `false`. +- **cone.maps.editable**: Flag whether to include ``Leaflet.Editable`` + plugin. Defaults to `false`. + - **cone.maps.activearea**: Flag whether to include ``Leaflet-active-area`` plugin. Defaults to `false`. diff --git a/src/cone/maps/browser/__init__.py b/src/cone/maps/browser/__init__.py index bb70dcd..cb182c7 100644 --- a/src/cone/maps/browser/__init__.py +++ b/src/cone/maps/browser/__init__.py @@ -73,6 +73,18 @@ resource='MarkerCluster.Default.css' )) +# Leaflet.Editable +leaflet_editable_resources = wr.ResourceGroup( + name='cone.maps-leaflet-editable', + directory=os.path.join(resources_dir, 'leaflet-editable'), + path='leaflet-editable' +) +leaflet_editable_resources.add(wr.ScriptResource( + name='leaflet-editable-js', + depends='leaflet-js', + resource='Leaflet.Editable.js' +)) + # Leaflet-active-area leaflet_activearea_resources = wr.ResourceGroup( name='cone.maps-leaflet-activearea', @@ -152,6 +164,11 @@ def included(name): config.set_resource_include('leaflet-markercluster-css', mc_include) config.set_resource_include('leaflet-markercluster-default-css', mc_include) + # Leaflet.Editable + config.register_resource(leaflet_editable_resources) + editable_include = include if included('cone.maps.editable') else False + config.set_resource_include('leaflet-editable-js', editable_include) + # Leaflet-active-area config.register_resource(leaflet_activearea_resources) activearea_include = include if included('cone.maps.activearea') else False diff --git a/src/cone/maps/browser/static/leaflet-editable/Leaflet.Editable.js b/src/cone/maps/browser/static/leaflet-editable/Leaflet.Editable.js new file mode 100644 index 0000000..c41b496 --- /dev/null +++ b/src/cone/maps/browser/static/leaflet-editable/Leaflet.Editable.js @@ -0,0 +1,1946 @@ +'use strict'; +(function (factory, window) { + /*globals define, module, require*/ + + // define an AMD module that relies on 'leaflet' + if (typeof define === 'function' && define.amd) { + define(['leaflet'], factory); + + + // define a Common JS module that relies on 'leaflet' + } else if (typeof exports === 'object') { + module.exports = factory(require('leaflet')); + } + + // attach your plugin to the global 'L' variable + if(typeof window !== 'undefined' && window.L){ + factory(window.L); + } + +}(function (L) { + // 🍂miniclass CancelableEvent (Event objects) + // 🍂method cancel() + // Cancel any subsequent action. + + // 🍂miniclass VertexEvent (Event objects) + // 🍂property vertex: VertexMarker + // The vertex that fires the event. + + // 🍂miniclass ShapeEvent (Event objects) + // 🍂property shape: Array + // The shape (LatLngs array) subject of the action. + + // 🍂miniclass CancelableVertexEvent (Event objects) + // 🍂inherits VertexEvent + // 🍂inherits CancelableEvent + + // 🍂miniclass CancelableShapeEvent (Event objects) + // 🍂inherits ShapeEvent + // 🍂inherits CancelableEvent + + // 🍂miniclass LayerEvent (Event objects) + // 🍂property layer: object + // The Layer (Marker, Polyline…) subject of the action. + + // 🍂namespace Editable; 🍂class Editable; 🍂aka L.Editable + // Main edition handler. By default, it is attached to the map + // as `map.editTools` property. + // Leaflet.Editable is made to be fully extendable. You have three ways to customize + // the behaviour: using options, listening to events, or extending. + L.Editable = L.Evented.extend({ + + statics: { + FORWARD: 1, + BACKWARD: -1 + }, + + options: { + + // You can pass them when creating a map using the `editOptions` key. + // 🍂option zIndex: int = 1000 + // The default zIndex of the editing tools. + zIndex: 1000, + + // 🍂option polygonClass: class = L.Polygon + // Class to be used when creating a new Polygon. + polygonClass: L.Polygon, + + // 🍂option polylineClass: class = L.Polyline + // Class to be used when creating a new Polyline. + polylineClass: L.Polyline, + + // 🍂option markerClass: class = L.Marker + // Class to be used when creating a new Marker. + markerClass: L.Marker, + + // 🍂option rectangleClass: class = L.Rectangle + // Class to be used when creating a new Rectangle. + rectangleClass: L.Rectangle, + + // 🍂option circleClass: class = L.Circle + // Class to be used when creating a new Circle. + circleClass: L.Circle, + + // 🍂option drawingCSSClass: string = 'leaflet-editable-drawing' + // CSS class to be added to the map container while drawing. + drawingCSSClass: 'leaflet-editable-drawing', + + // 🍂option drawingCursor: const = 'crosshair' + // Cursor mode set to the map while drawing. + drawingCursor: 'crosshair', + + // 🍂option editLayer: Layer = new L.LayerGroup() + // Layer used to store edit tools (vertex, line guide…). + editLayer: undefined, + + // 🍂option featuresLayer: Layer = new L.LayerGroup() + // Default layer used to store drawn features (Marker, Polyline…). + featuresLayer: undefined, + + // 🍂option polylineEditorClass: class = PolylineEditor + // Class to be used as Polyline editor. + polylineEditorClass: undefined, + + // 🍂option polygonEditorClass: class = PolygonEditor + // Class to be used as Polygon editor. + polygonEditorClass: undefined, + + // 🍂option markerEditorClass: class = MarkerEditor + // Class to be used as Marker editor. + markerEditorClass: undefined, + + // 🍂option rectangleEditorClass: class = RectangleEditor + // Class to be used as Rectangle editor. + rectangleEditorClass: undefined, + + // 🍂option circleEditorClass: class = CircleEditor + // Class to be used as Circle editor. + circleEditorClass: undefined, + + // 🍂option lineGuideOptions: hash = {} + // Options to be passed to the line guides. + lineGuideOptions: {}, + + // 🍂option skipMiddleMarkers: boolean = false + // Set this to true if you don't want middle markers. + skipMiddleMarkers: false + + }, + + initialize: function (map, options) { + L.setOptions(this, options); + this._lastZIndex = this.options.zIndex; + this.map = map; + this.editLayer = this.createEditLayer(); + this.featuresLayer = this.createFeaturesLayer(); + this.forwardLineGuide = this.createLineGuide(); + this.backwardLineGuide = this.createLineGuide(); + }, + + fireAndForward: function (type, e) { + e = e || {}; + e.editTools = this; + this.fire(type, e); + this.map.fire(type, e); + }, + + createLineGuide: function () { + var options = L.extend({dashArray: '5,10', weight: 1, interactive: false}, this.options.lineGuideOptions); + return L.polyline([], options); + }, + + createVertexIcon: function (options) { + return L.Browser.mobile && L.Browser.touch ? new L.Editable.TouchVertexIcon(options) : new L.Editable.VertexIcon(options); + }, + + createEditLayer: function () { + return this.options.editLayer || new L.LayerGroup().addTo(this.map); + }, + + createFeaturesLayer: function () { + return this.options.featuresLayer || new L.LayerGroup().addTo(this.map); + }, + + moveForwardLineGuide: function (latlng) { + if (this.forwardLineGuide._latlngs.length) { + this.forwardLineGuide._latlngs[1] = latlng; + this.forwardLineGuide._bounds.extend(latlng); + this.forwardLineGuide.redraw(); + } + }, + + moveBackwardLineGuide: function (latlng) { + if (this.backwardLineGuide._latlngs.length) { + this.backwardLineGuide._latlngs[1] = latlng; + this.backwardLineGuide._bounds.extend(latlng); + this.backwardLineGuide.redraw(); + } + }, + + anchorForwardLineGuide: function (latlng) { + this.forwardLineGuide._latlngs[0] = latlng; + this.forwardLineGuide._bounds.extend(latlng); + this.forwardLineGuide.redraw(); + }, + + anchorBackwardLineGuide: function (latlng) { + this.backwardLineGuide._latlngs[0] = latlng; + this.backwardLineGuide._bounds.extend(latlng); + this.backwardLineGuide.redraw(); + }, + + attachForwardLineGuide: function () { + this.editLayer.addLayer(this.forwardLineGuide); + }, + + attachBackwardLineGuide: function () { + this.editLayer.addLayer(this.backwardLineGuide); + }, + + detachForwardLineGuide: function () { + this.forwardLineGuide.setLatLngs([]); + this.editLayer.removeLayer(this.forwardLineGuide); + }, + + detachBackwardLineGuide: function () { + this.backwardLineGuide.setLatLngs([]); + this.editLayer.removeLayer(this.backwardLineGuide); + }, + + blockEvents: function () { + // Hack: force map not to listen to other layers events while drawing. + if (!this._oldTargets) { + this._oldTargets = this.map._targets; + this.map._targets = {}; + } + }, + + unblockEvents: function () { + if (this._oldTargets) { + // Reset, but keep targets created while drawing. + this.map._targets = L.extend(this.map._targets, this._oldTargets); + delete this._oldTargets; + } + }, + + registerForDrawing: function (editor) { + if (this._drawingEditor) this.unregisterForDrawing(this._drawingEditor); + this.blockEvents(); + editor.reset(); // Make sure editor tools still receive events. + this._drawingEditor = editor; + this.map.on('mousemove touchmove', editor.onDrawingMouseMove, editor); + this.map.on('mousedown', this.onMousedown, this); + this.map.on('mouseup', this.onMouseup, this); + L.DomUtil.addClass(this.map._container, this.options.drawingCSSClass); + this.defaultMapCursor = this.map._container.style.cursor; + this.map._container.style.cursor = this.options.drawingCursor; + }, + + unregisterForDrawing: function (editor) { + this.unblockEvents(); + L.DomUtil.removeClass(this.map._container, this.options.drawingCSSClass); + this.map._container.style.cursor = this.defaultMapCursor; + editor = editor || this._drawingEditor; + if (!editor) return; + this.map.off('mousemove touchmove', editor.onDrawingMouseMove, editor); + this.map.off('mousedown', this.onMousedown, this); + this.map.off('mouseup', this.onMouseup, this); + if (editor !== this._drawingEditor) return; + delete this._drawingEditor; + if (editor._drawing) editor.cancelDrawing(); + }, + + onMousedown: function (e) { + if (e.originalEvent.which != 1) return; + this._mouseDown = e; + this._drawingEditor.onDrawingMouseDown(e); + }, + + onMouseup: function (e) { + if (this._mouseDown) { + var editor = this._drawingEditor, + mouseDown = this._mouseDown; + this._mouseDown = null; + editor.onDrawingMouseUp(e); + if (this._drawingEditor !== editor) return; // onDrawingMouseUp may call unregisterFromDrawing. + var origin = L.point(mouseDown.originalEvent.clientX, mouseDown.originalEvent.clientY); + var distance = L.point(e.originalEvent.clientX, e.originalEvent.clientY).distanceTo(origin); + if (Math.abs(distance) < 9 * (window.devicePixelRatio || 1)) this._drawingEditor.onDrawingClick(e); + } + }, + + // 🍂section Public methods + // You will generally access them by the `map.editTools` + // instance: + // + // `map.editTools.startPolyline();` + + // 🍂method drawing(): boolean + // Return true if any drawing action is ongoing. + drawing: function () { + return this._drawingEditor && this._drawingEditor.drawing(); + }, + + // 🍂method stopDrawing() + // When you need to stop any ongoing drawing, without needing to know which editor is active. + stopDrawing: function () { + this.unregisterForDrawing(); + }, + + // 🍂method commitDrawing() + // When you need to commit any ongoing drawing, without needing to know which editor is active. + commitDrawing: function (e) { + if (!this._drawingEditor) return; + this._drawingEditor.commitDrawing(e); + }, + + connectCreatedToMap: function (layer) { + return this.featuresLayer.addLayer(layer); + }, + + // 🍂method startPolyline(latlng: L.LatLng, options: hash): L.Polyline + // Start drawing a Polyline. If `latlng` is given, a first point will be added. In any case, continuing on user click. + // If `options` is given, it will be passed to the Polyline class constructor. + startPolyline: function (latlng, options) { + var line = this.createPolyline([], options); + line.enableEdit(this.map).newShape(latlng); + return line; + }, + + // 🍂method startPolygon(latlng: L.LatLng, options: hash): L.Polygon + // Start drawing a Polygon. If `latlng` is given, a first point will be added. In any case, continuing on user click. + // If `options` is given, it will be passed to the Polygon class constructor. + startPolygon: function (latlng, options) { + var polygon = this.createPolygon([], options); + polygon.enableEdit(this.map).newShape(latlng); + return polygon; + }, + + // 🍂method startMarker(latlng: L.LatLng, options: hash): L.Marker + // Start adding a Marker. If `latlng` is given, the Marker will be shown first at this point. + // In any case, it will follow the user mouse, and will have a final `latlng` on next click (or touch). + // If `options` is given, it will be passed to the Marker class constructor. + startMarker: function (latlng, options) { + latlng = latlng || this.map.getCenter().clone(); + var marker = this.createMarker(latlng, options); + marker.enableEdit(this.map).startDrawing(); + return marker; + }, + + // 🍂method startRectangle(latlng: L.LatLng, options: hash): L.Rectangle + // Start drawing a Rectangle. If `latlng` is given, the Rectangle anchor will be added. In any case, continuing on user drag. + // If `options` is given, it will be passed to the Rectangle class constructor. + startRectangle: function(latlng, options) { + var corner = latlng || L.latLng([0, 0]); + var bounds = new L.LatLngBounds(corner, corner); + var rectangle = this.createRectangle(bounds, options); + rectangle.enableEdit(this.map).startDrawing(); + return rectangle; + }, + + // 🍂method startCircle(latlng: L.LatLng, options: hash): L.Circle + // Start drawing a Circle. If `latlng` is given, the Circle anchor will be added. In any case, continuing on user drag. + // If `options` is given, it will be passed to the Circle class constructor. + startCircle: function (latlng, options) { + latlng = latlng || this.map.getCenter().clone(); + var circle = this.createCircle(latlng, options); + circle.enableEdit(this.map).startDrawing(); + return circle; + }, + + startHole: function (editor, latlng) { + editor.newHole(latlng); + }, + + createLayer: function (klass, latlngs, options) { + options = L.Util.extend({editOptions: {editTools: this}}, options); + var layer = new klass(latlngs, options); + // 🍂namespace Editable + // 🍂event editable:created: LayerEvent + // Fired when a new feature (Marker, Polyline…) is created. + this.fireAndForward('editable:created', {layer: layer}); + return layer; + }, + + createPolyline: function (latlngs, options) { + return this.createLayer(options && options.polylineClass || this.options.polylineClass, latlngs, options); + }, + + createPolygon: function (latlngs, options) { + return this.createLayer(options && options.polygonClass || this.options.polygonClass, latlngs, options); + }, + + createMarker: function (latlng, options) { + return this.createLayer(options && options.markerClass || this.options.markerClass, latlng, options); + }, + + createRectangle: function (bounds, options) { + return this.createLayer(options && options.rectangleClass || this.options.rectangleClass, bounds, options); + }, + + createCircle: function (latlng, options) { + return this.createLayer(options && options.circleClass || this.options.circleClass, latlng, options); + } + + }); + + L.extend(L.Editable, { + + makeCancellable: function (e) { + e.cancel = function () { + e._cancelled = true; + }; + } + + }); + + // 🍂namespace Map; 🍂class Map + // Leaflet.Editable add options and events to the `L.Map` object. + // See `Editable` events for the list of events fired on the Map. + // 🍂example + // + // ```js + // var map = L.map('map', { + // editable: true, + // editOptions: { + // … + // } + // }); + // ``` + // 🍂section Editable Map Options + L.Map.mergeOptions({ + + // 🍂namespace Map + // 🍂section Map Options + // 🍂option editToolsClass: class = L.Editable + // Class to be used as vertex, for path editing. + editToolsClass: L.Editable, + + // 🍂option editable: boolean = false + // Whether to create a L.Editable instance at map init. + editable: false, + + // 🍂option editOptions: hash = {} + // Options to pass to L.Editable when instantiating. + editOptions: {} + + }); + + L.Map.addInitHook(function () { + + this.whenReady(function () { + if (this.options.editable) { + this.editTools = new this.options.editToolsClass(this, this.options.editOptions); + } + }); + + }); + + L.Editable.VertexIcon = L.DivIcon.extend({ + + options: { + iconSize: new L.Point(8, 8) + } + + }); + + L.Editable.TouchVertexIcon = L.Editable.VertexIcon.extend({ + + options: { + iconSize: new L.Point(20, 20) + } + + }); + + + // 🍂namespace Editable; 🍂class VertexMarker; Handler for dragging path vertices. + L.Editable.VertexMarker = L.Marker.extend({ + + options: { + draggable: true, + className: 'leaflet-div-icon leaflet-vertex-icon' + }, + + + // 🍂section Public methods + // The marker used to handle path vertex. You will usually interact with a `VertexMarker` + // instance when listening for events like `editable:vertex:ctrlclick`. + + initialize: function (latlng, latlngs, editor, options) { + // We don't use this._latlng, because on drag Leaflet replace it while + // we want to keep reference. + this.latlng = latlng; + this.latlngs = latlngs; + this.editor = editor; + L.Marker.prototype.initialize.call(this, latlng, options); + this.options.icon = this.editor.tools.createVertexIcon({className: this.options.className}); + this.latlng.__vertex = this; + this.editor.editLayer.addLayer(this); + this.setZIndexOffset(editor.tools._lastZIndex + 1); + }, + + onAdd: function (map) { + L.Marker.prototype.onAdd.call(this, map); + this.on('drag', this.onDrag); + this.on('dragstart', this.onDragStart); + this.on('dragend', this.onDragEnd); + this.on('mouseup', this.onMouseup); + this.on('click', this.onClick); + this.on('contextmenu', this.onContextMenu); + this.on('mousedown touchstart', this.onMouseDown); + this.on('mouseover', this.onMouseOver); + this.on('mouseout', this.onMouseOut); + this.addMiddleMarkers(); + }, + + onRemove: function (map) { + if (this.middleMarker) this.middleMarker.delete(); + delete this.latlng.__vertex; + this.off('drag', this.onDrag); + this.off('dragstart', this.onDragStart); + this.off('dragend', this.onDragEnd); + this.off('mouseup', this.onMouseup); + this.off('click', this.onClick); + this.off('contextmenu', this.onContextMenu); + this.off('mousedown touchstart', this.onMouseDown); + this.off('mouseover', this.onMouseOver); + this.off('mouseout', this.onMouseOut); + L.Marker.prototype.onRemove.call(this, map); + }, + + onDrag: function (e) { + e.vertex = this; + this.editor.onVertexMarkerDrag(e); + var iconPos = L.DomUtil.getPosition(this._icon), + latlng = this._map.layerPointToLatLng(iconPos); + this.latlng.update(latlng); + this._latlng = this.latlng; // Push back to Leaflet our reference. + this.editor.refresh(); + if (this.middleMarker) this.middleMarker.updateLatLng(); + var next = this.getNext(); + if (next && next.middleMarker) next.middleMarker.updateLatLng(); + }, + + onDragStart: function (e) { + e.vertex = this; + this.editor.onVertexMarkerDragStart(e); + }, + + onDragEnd: function (e) { + e.vertex = this; + this.editor.onVertexMarkerDragEnd(e); + }, + + onClick: function (e) { + e.vertex = this; + this.editor.onVertexMarkerClick(e); + }, + + onMouseup: function (e) { + L.DomEvent.stop(e); + e.vertex = this; + this.editor.map.fire('mouseup', e); + }, + + onContextMenu: function (e) { + e.vertex = this; + this.editor.onVertexMarkerContextMenu(e); + }, + + onMouseDown: function (e) { + e.vertex = this; + this.editor.onVertexMarkerMouseDown(e); + }, + + onMouseOver: function (e) { + e.vertex = this; + this.editor.onVertexMarkerMouseOver(e); + }, + + onMouseOut: function (e) { + e.vertex = this; + this.editor.onVertexMarkerMouseOut(e); + }, + + // 🍂method delete() + // Delete a vertex and the related LatLng. + delete: function () { + var next = this.getNext(); // Compute before changing latlng + this.latlngs.splice(this.getIndex(), 1); + this.editor.editLayer.removeLayer(this); + this.editor.onVertexDeleted({latlng: this.latlng, vertex: this}); + if (!this.latlngs.length) this.editor.deleteShape(this.latlngs); + if (next) next.resetMiddleMarker(); + this.editor.refresh(); + }, + + // 🍂method getIndex(): int + // Get the index of the current vertex among others of the same LatLngs group. + getIndex: function () { + return this.latlngs.indexOf(this.latlng); + }, + + // 🍂method getLastIndex(): int + // Get last vertex index of the LatLngs group of the current vertex. + getLastIndex: function () { + return this.latlngs.length - 1; + }, + + // 🍂method getPrevious(): VertexMarker + // Get the previous VertexMarker in the same LatLngs group. + getPrevious: function () { + if (this.latlngs.length < 2) return; + var index = this.getIndex(), + previousIndex = index - 1; + if (index === 0 && this.editor.CLOSED) previousIndex = this.getLastIndex(); + var previous = this.latlngs[previousIndex]; + if (previous) return previous.__vertex; + }, + + // 🍂method getNext(): VertexMarker + // Get the next VertexMarker in the same LatLngs group. + getNext: function () { + if (this.latlngs.length < 2) return; + var index = this.getIndex(), + nextIndex = index + 1; + if (index === this.getLastIndex() && this.editor.CLOSED) nextIndex = 0; + var next = this.latlngs[nextIndex]; + if (next) return next.__vertex; + }, + + addMiddleMarker: function (previous) { + if (!this.editor.hasMiddleMarkers()) return; + previous = previous || this.getPrevious(); + if (previous && !this.middleMarker) this.middleMarker = this.editor.addMiddleMarker(previous, this, this.latlngs, this.editor); + }, + + addMiddleMarkers: function () { + if (!this.editor.hasMiddleMarkers()) return; + var previous = this.getPrevious(); + if (previous) this.addMiddleMarker(previous); + var next = this.getNext(); + if (next) next.resetMiddleMarker(); + }, + + resetMiddleMarker: function () { + if (this.middleMarker) this.middleMarker.delete(); + this.addMiddleMarker(); + }, + + // 🍂method split() + // Split the vertex LatLngs group at its index, if possible. + split: function () { + if (!this.editor.splitShape) return; // Only for PolylineEditor + this.editor.splitShape(this.latlngs, this.getIndex()); + }, + + // 🍂method continue() + // Continue the vertex LatLngs from this vertex. Only active for first and last vertices of a Polyline. + continue: function () { + if (!this.editor.continueBackward) return; // Only for PolylineEditor + var index = this.getIndex(); + if (index === 0) this.editor.continueBackward(this.latlngs); + else if (index === this.getLastIndex()) this.editor.continueForward(this.latlngs); + } + + }); + + L.Editable.mergeOptions({ + + // 🍂namespace Editable + // 🍂option vertexMarkerClass: class = VertexMarker + // Class to be used as vertex, for path editing. + vertexMarkerClass: L.Editable.VertexMarker + + }); + + L.Editable.MiddleMarker = L.Marker.extend({ + + options: { + opacity: 0.5, + className: 'leaflet-div-icon leaflet-middle-icon', + draggable: true + }, + + initialize: function (left, right, latlngs, editor, options) { + this.left = left; + this.right = right; + this.editor = editor; + this.latlngs = latlngs; + L.Marker.prototype.initialize.call(this, this.computeLatLng(), options); + this._opacity = this.options.opacity; + this.options.icon = this.editor.tools.createVertexIcon({className: this.options.className}); + this.editor.editLayer.addLayer(this); + this.setVisibility(); + }, + + setVisibility: function () { + var leftPoint = this._map.latLngToContainerPoint(this.left.latlng), + rightPoint = this._map.latLngToContainerPoint(this.right.latlng), + size = L.point(this.options.icon.options.iconSize); + if (leftPoint.distanceTo(rightPoint) < size.x * 3) this.hide(); + else this.show(); + }, + + show: function () { + this.setOpacity(this._opacity); + }, + + hide: function () { + this.setOpacity(0); + }, + + updateLatLng: function () { + this.setLatLng(this.computeLatLng()); + this.setVisibility(); + }, + + computeLatLng: function () { + var leftPoint = this.editor.map.latLngToContainerPoint(this.left.latlng), + rightPoint = this.editor.map.latLngToContainerPoint(this.right.latlng), + y = (leftPoint.y + rightPoint.y) / 2, + x = (leftPoint.x + rightPoint.x) / 2; + return this.editor.map.containerPointToLatLng([x, y]); + }, + + onAdd: function (map) { + L.Marker.prototype.onAdd.call(this, map); + L.DomEvent.on(this._icon, 'mousedown touchstart', this.onMouseDown, this); + map.on('zoomend', this.setVisibility, this); + }, + + onRemove: function (map) { + delete this.right.middleMarker; + L.DomEvent.off(this._icon, 'mousedown touchstart', this.onMouseDown, this); + map.off('zoomend', this.setVisibility, this); + L.Marker.prototype.onRemove.call(this, map); + }, + + onMouseDown: function (e) { + var iconPos = L.DomUtil.getPosition(this._icon), + latlng = this.editor.map.layerPointToLatLng(iconPos); + e = { + originalEvent: e, + latlng: latlng + }; + if (this.options.opacity === 0) return; + L.Editable.makeCancellable(e); + this.editor.onMiddleMarkerMouseDown(e); + if (e._cancelled) return; + this.latlngs.splice(this.index(), 0, e.latlng); + this.editor.refresh(); + var icon = this._icon; + var marker = this.editor.addVertexMarker(e.latlng, this.latlngs); + this.editor.onNewVertex(marker); + /* Hack to workaround browser not firing touchend when element is no more on DOM */ + var parent = marker._icon.parentNode; + parent.removeChild(marker._icon); + marker._icon = icon; + parent.appendChild(marker._icon); + marker._initIcon(); + marker._initInteraction(); + marker.setOpacity(1); + /* End hack */ + // Transfer ongoing dragging to real marker + L.Draggable._dragging = false; + marker.dragging._draggable._onDown(e.originalEvent); + this.delete(); + }, + + delete: function () { + this.editor.editLayer.removeLayer(this); + }, + + index: function () { + return this.latlngs.indexOf(this.right.latlng); + } + + }); + + L.Editable.mergeOptions({ + + // 🍂namespace Editable + // 🍂option middleMarkerClass: class = VertexMarker + // Class to be used as middle vertex, pulled by the user to create a new point in the middle of a path. + middleMarkerClass: L.Editable.MiddleMarker + + }); + + // 🍂namespace Editable; 🍂class BaseEditor; 🍂aka L.Editable.BaseEditor + // When editing a feature (Marker, Polyline…), an editor is attached to it. This + // editor basically knows how to handle the edition. + L.Editable.BaseEditor = L.Handler.extend({ + + initialize: function (map, feature, options) { + L.setOptions(this, options); + this.map = map; + this.feature = feature; + this.feature.editor = this; + this.editLayer = new L.LayerGroup(); + this.tools = this.options.editTools || map.editTools; + }, + + // 🍂method enable(): this + // Set up the drawing tools for the feature to be editable. + addHooks: function () { + if (this.isConnected()) this.onFeatureAdd(); + else this.feature.once('add', this.onFeatureAdd, this); + this.onEnable(); + this.feature.on(this._getEvents(), this); + }, + + // 🍂method disable(): this + // Remove the drawing tools for the feature. + removeHooks: function () { + this.feature.off(this._getEvents(), this); + if (this.feature.dragging) this.feature.dragging.disable(); + this.editLayer.clearLayers(); + this.tools.editLayer.removeLayer(this.editLayer); + this.onDisable(); + if (this._drawing) this.cancelDrawing(); + }, + + // 🍂method drawing(): boolean + // Return true if any drawing action is ongoing with this editor. + drawing: function () { + return !!this._drawing; + }, + + reset: function () {}, + + onFeatureAdd: function () { + this.tools.editLayer.addLayer(this.editLayer); + if (this.feature.dragging) this.feature.dragging.enable(); + }, + + hasMiddleMarkers: function () { + return !this.options.skipMiddleMarkers && !this.tools.options.skipMiddleMarkers; + }, + + fireAndForward: function (type, e) { + e = e || {}; + e.layer = this.feature; + this.feature.fire(type, e); + this.tools.fireAndForward(type, e); + }, + + onEnable: function () { + // 🍂namespace Editable + // 🍂event editable:enable: Event + // Fired when an existing feature is ready to be edited. + this.fireAndForward('editable:enable'); + }, + + onDisable: function () { + // 🍂namespace Editable + // 🍂event editable:disable: Event + // Fired when an existing feature is not ready anymore to be edited. + this.fireAndForward('editable:disable'); + }, + + onEditing: function () { + // 🍂namespace Editable + // 🍂event editable:editing: Event + // Fired as soon as any change is made to the feature geometry. + this.fireAndForward('editable:editing'); + }, + + onStartDrawing: function () { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:start: Event + // Fired when a feature is to be drawn. + this.fireAndForward('editable:drawing:start'); + }, + + onEndDrawing: function () { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:end: Event + // Fired when a feature is not drawn anymore. + this.fireAndForward('editable:drawing:end'); + }, + + onCancelDrawing: function () { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:cancel: Event + // Fired when user cancel drawing while a feature is being drawn. + this.fireAndForward('editable:drawing:cancel'); + }, + + onCommitDrawing: function (e) { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:commit: Event + // Fired when user finish drawing a feature. + this.fireAndForward('editable:drawing:commit', e); + }, + + onDrawingMouseDown: function (e) { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:mousedown: Event + // Fired when user `mousedown` while drawing. + this.fireAndForward('editable:drawing:mousedown', e); + }, + + onDrawingMouseUp: function (e) { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:mouseup: Event + // Fired when user `mouseup` while drawing. + this.fireAndForward('editable:drawing:mouseup', e); + }, + + startDrawing: function () { + if (!this._drawing) this._drawing = L.Editable.FORWARD; + this.tools.registerForDrawing(this); + this.onStartDrawing(); + }, + + commitDrawing: function (e) { + this.onCommitDrawing(e); + this.endDrawing(); + }, + + cancelDrawing: function () { + // If called during a vertex drag, the vertex will be removed before + // the mouseup fires on it. This is a workaround. Maybe better fix is + // To have L.Draggable reset it's status on disable (Leaflet side). + L.Draggable._dragging = false; + this.onCancelDrawing(); + this.endDrawing(); + }, + + endDrawing: function () { + this._drawing = false; + this.tools.unregisterForDrawing(this); + this.onEndDrawing(); + }, + + onDrawingClick: function (e) { + if (!this.drawing()) return; + L.Editable.makeCancellable(e); + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:click: CancelableEvent + // Fired when user `click` while drawing, before any internal action is being processed. + this.fireAndForward('editable:drawing:click', e); + if (e._cancelled) return; + if (!this.isConnected()) this.connect(e); + this.processDrawingClick(e); + }, + + isConnected: function () { + return this.map.hasLayer(this.feature); + }, + + connect: function () { + this.tools.connectCreatedToMap(this.feature); + this.tools.editLayer.addLayer(this.editLayer); + }, + + onMove: function (e) { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:move: Event + // Fired when `move` mouse while drawing, while dragging a marker, and while dragging a vertex. + this.fireAndForward('editable:drawing:move', e); + }, + + onDrawingMouseMove: function (e) { + this.onMove(e); + }, + + _getEvents: function () { + return { + dragstart: this.onDragStart, + drag: this.onDrag, + dragend: this.onDragEnd, + remove: this.disable + }; + }, + + onDragStart: function (e) { + this.onEditing(); + // 🍂namespace Editable + // 🍂event editable:dragstart: Event + // Fired before a path feature is dragged. + this.fireAndForward('editable:dragstart', e); + }, + + onDrag: function (e) { + this.onMove(e); + // 🍂namespace Editable + // 🍂event editable:drag: Event + // Fired when a path feature is being dragged. + this.fireAndForward('editable:drag', e); + }, + + onDragEnd: function (e) { + // 🍂namespace Editable + // 🍂event editable:dragend: Event + // Fired after a path feature has been dragged. + this.fireAndForward('editable:dragend', e); + } + + }); + + // 🍂namespace Editable; 🍂class MarkerEditor; 🍂aka L.Editable.MarkerEditor + // 🍂inherits BaseEditor + // Editor for Marker. + L.Editable.MarkerEditor = L.Editable.BaseEditor.extend({ + + onDrawingMouseMove: function (e) { + L.Editable.BaseEditor.prototype.onDrawingMouseMove.call(this, e); + if (this._drawing) this.feature.setLatLng(e.latlng); + }, + + processDrawingClick: function (e) { + // 🍂namespace Editable + // 🍂section Drawing events + // 🍂event editable:drawing:clicked: Event + // Fired when user `click` while drawing, after all internal actions. + this.fireAndForward('editable:drawing:clicked', e); + this.commitDrawing(e); + }, + + connect: function (e) { + // On touch, the latlng has not been updated because there is + // no mousemove. + if (e) this.feature._latlng = e.latlng; + L.Editable.BaseEditor.prototype.connect.call(this, e); + } + + }); + + // 🍂namespace Editable; 🍂class PathEditor; 🍂aka L.Editable.PathEditor + // 🍂inherits BaseEditor + // Base class for all path editors. + L.Editable.PathEditor = L.Editable.BaseEditor.extend({ + + CLOSED: false, + MIN_VERTEX: 2, + + addHooks: function () { + L.Editable.BaseEditor.prototype.addHooks.call(this); + if (this.feature) this.initVertexMarkers(); + return this; + }, + + initVertexMarkers: function (latlngs) { + if (!this.enabled()) return; + latlngs = latlngs || this.getLatLngs(); + if (isFlat(latlngs)) this.addVertexMarkers(latlngs); + else for (var i = 0; i < latlngs.length; i++) this.initVertexMarkers(latlngs[i]); + }, + + getLatLngs: function () { + return this.feature.getLatLngs(); + }, + + // 🍂method reset() + // Rebuild edit elements (Vertex, MiddleMarker, etc.). + reset: function () { + this.editLayer.clearLayers(); + this.initVertexMarkers(); + }, + + addVertexMarker: function (latlng, latlngs) { + return new this.tools.options.vertexMarkerClass(latlng, latlngs, this); + }, + + onNewVertex: function (vertex) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:new: VertexEvent + // Fired when a new vertex is created. + this.fireAndForward('editable:vertex:new', {latlng: vertex.latlng, vertex: vertex}); + }, + + addVertexMarkers: function (latlngs) { + for (var i = 0; i < latlngs.length; i++) { + this.addVertexMarker(latlngs[i], latlngs); + } + }, + + refreshVertexMarkers: function (latlngs) { + latlngs = latlngs || this.getDefaultLatLngs(); + for (var i = 0; i < latlngs.length; i++) { + latlngs[i].__vertex.update(); + } + }, + + addMiddleMarker: function (left, right, latlngs) { + return new this.tools.options.middleMarkerClass(left, right, latlngs, this); + }, + + onVertexMarkerClick: function (e) { + L.Editable.makeCancellable(e); + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:click: CancelableVertexEvent + // Fired when a `click` is issued on a vertex, before any internal action is being processed. + this.fireAndForward('editable:vertex:click', e); + if (e._cancelled) return; + if (this.tools.drawing() && this.tools._drawingEditor !== this) return; + var index = e.vertex.getIndex(), commit; + if (e.originalEvent.ctrlKey) { + this.onVertexMarkerCtrlClick(e); + } else if (e.originalEvent.altKey) { + this.onVertexMarkerAltClick(e); + } else if (e.originalEvent.shiftKey) { + this.onVertexMarkerShiftClick(e); + } else if (e.originalEvent.metaKey) { + this.onVertexMarkerMetaKeyClick(e); + } else if (index === e.vertex.getLastIndex() && this._drawing === L.Editable.FORWARD) { + if (index >= this.MIN_VERTEX - 1) commit = true; + } else if (index === 0 && this._drawing === L.Editable.BACKWARD && this._drawnLatLngs.length >= this.MIN_VERTEX) { + commit = true; + } else if (index === 0 && this._drawing === L.Editable.FORWARD && this._drawnLatLngs.length >= this.MIN_VERTEX && this.CLOSED) { + commit = true; // Allow to close on first point also for polygons + } else { + this.onVertexRawMarkerClick(e); + } + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:clicked: VertexEvent + // Fired when a `click` is issued on a vertex, after all internal actions. + this.fireAndForward('editable:vertex:clicked', e); + if (commit) this.commitDrawing(e); + }, + + onVertexRawMarkerClick: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:rawclick: CancelableVertexEvent + // Fired when a `click` is issued on a vertex without any special key and without being in drawing mode. + this.fireAndForward('editable:vertex:rawclick', e); + if (e._cancelled) return; + if (!this.vertexCanBeDeleted(e.vertex)) return; + e.vertex.delete(); + }, + + vertexCanBeDeleted: function (vertex) { + return vertex.latlngs.length > this.MIN_VERTEX; + }, + + onVertexDeleted: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:deleted: VertexEvent + // Fired after a vertex has been deleted by user. + this.fireAndForward('editable:vertex:deleted', e); + }, + + onVertexMarkerCtrlClick: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:ctrlclick: VertexEvent + // Fired when a `click` with `ctrlKey` is issued on a vertex. + this.fireAndForward('editable:vertex:ctrlclick', e); + }, + + onVertexMarkerShiftClick: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:shiftclick: VertexEvent + // Fired when a `click` with `shiftKey` is issued on a vertex. + this.fireAndForward('editable:vertex:shiftclick', e); + }, + + onVertexMarkerMetaKeyClick: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:metakeyclick: VertexEvent + // Fired when a `click` with `metaKey` is issued on a vertex. + this.fireAndForward('editable:vertex:metakeyclick', e); + }, + + onVertexMarkerAltClick: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:altclick: VertexEvent + // Fired when a `click` with `altKey` is issued on a vertex. + this.fireAndForward('editable:vertex:altclick', e); + }, + + onVertexMarkerContextMenu: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:contextmenu: VertexEvent + // Fired when a `contextmenu` is issued on a vertex. + this.fireAndForward('editable:vertex:contextmenu', e); + }, + + onVertexMarkerMouseDown: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:mousedown: VertexEvent + // Fired when user `mousedown` a vertex. + this.fireAndForward('editable:vertex:mousedown', e); + }, + + onVertexMarkerMouseOver: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:mouseover: VertexEvent + // Fired when a user's mouse enters the vertex + this.fireAndForward('editable:vertex:mouseover', e); + }, + + onVertexMarkerMouseOut: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:mouseout: VertexEvent + // Fired when a user's mouse leaves the vertex + this.fireAndForward('editable:vertex:mouseout', e); + }, + + onMiddleMarkerMouseDown: function (e) { + // 🍂namespace Editable + // 🍂section MiddleMarker events + // 🍂event editable:middlemarker:mousedown: VertexEvent + // Fired when user `mousedown` a middle marker. + this.fireAndForward('editable:middlemarker:mousedown', e); + }, + + onVertexMarkerDrag: function (e) { + this.onMove(e); + if (this.feature._bounds) this.extendBounds(e); + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:drag: VertexEvent + // Fired when a vertex is dragged by user. + this.fireAndForward('editable:vertex:drag', e); + }, + + onVertexMarkerDragStart: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:dragstart: VertexEvent + // Fired before a vertex is dragged by user. + this.fireAndForward('editable:vertex:dragstart', e); + }, + + onVertexMarkerDragEnd: function (e) { + // 🍂namespace Editable + // 🍂section Vertex events + // 🍂event editable:vertex:dragend: VertexEvent + // Fired after a vertex is dragged by user. + this.fireAndForward('editable:vertex:dragend', e); + }, + + setDrawnLatLngs: function (latlngs) { + this._drawnLatLngs = latlngs || this.getDefaultLatLngs(); + }, + + startDrawing: function () { + if (!this._drawnLatLngs) this.setDrawnLatLngs(); + L.Editable.BaseEditor.prototype.startDrawing.call(this); + }, + + startDrawingForward: function () { + this.startDrawing(); + }, + + endDrawing: function () { + this.tools.detachForwardLineGuide(); + this.tools.detachBackwardLineGuide(); + if (this._drawnLatLngs && this._drawnLatLngs.length < this.MIN_VERTEX) this.deleteShape(this._drawnLatLngs); + L.Editable.BaseEditor.prototype.endDrawing.call(this); + delete this._drawnLatLngs; + }, + + addLatLng: function (latlng) { + if (this._drawing === L.Editable.FORWARD) this._drawnLatLngs.push(latlng); + else this._drawnLatLngs.unshift(latlng); + this.feature._bounds.extend(latlng); + var vertex = this.addVertexMarker(latlng, this._drawnLatLngs); + this.onNewVertex(vertex); + this.refresh(); + }, + + newPointForward: function (latlng) { + this.addLatLng(latlng); + this.tools.attachForwardLineGuide(); + this.tools.anchorForwardLineGuide(latlng); + }, + + newPointBackward: function (latlng) { + this.addLatLng(latlng); + this.tools.anchorBackwardLineGuide(latlng); + }, + + // 🍂namespace PathEditor + // 🍂method push() + // Programmatically add a point while drawing. + push: function (latlng) { + if (!latlng) return console.error('L.Editable.PathEditor.push expect a valid latlng as parameter'); + if (this._drawing === L.Editable.FORWARD) this.newPointForward(latlng); + else this.newPointBackward(latlng); + }, + + removeLatLng: function (latlng) { + latlng.__vertex.delete(); + this.refresh(); + }, + + // 🍂method pop(): L.LatLng or null + // Programmatically remove last point (if any) while drawing. + pop: function () { + if (this._drawnLatLngs.length <= 1) return; + var latlng; + if (this._drawing === L.Editable.FORWARD) latlng = this._drawnLatLngs[this._drawnLatLngs.length - 1]; + else latlng = this._drawnLatLngs[0]; + this.removeLatLng(latlng); + if (this._drawing === L.Editable.FORWARD) this.tools.anchorForwardLineGuide(this._drawnLatLngs[this._drawnLatLngs.length - 1]); + else this.tools.anchorForwardLineGuide(this._drawnLatLngs[0]); + return latlng; + }, + + processDrawingClick: function (e) { + if (e.vertex && e.vertex.editor === this) return; + if (this._drawing === L.Editable.FORWARD) this.newPointForward(e.latlng); + else this.newPointBackward(e.latlng); + this.fireAndForward('editable:drawing:clicked', e); + }, + + onDrawingMouseMove: function (e) { + L.Editable.BaseEditor.prototype.onDrawingMouseMove.call(this, e); + if (this._drawing) { + this.tools.moveForwardLineGuide(e.latlng); + this.tools.moveBackwardLineGuide(e.latlng); + } + }, + + refresh: function () { + this.feature.redraw(); + this.onEditing(); + }, + + // 🍂namespace PathEditor + // 🍂method newShape(latlng?: L.LatLng) + // Add a new shape (Polyline, Polygon) in a multi, and setup up drawing tools to draw it; + // if optional `latlng` is given, start a path at this point. + newShape: function (latlng) { + var shape = this.addNewEmptyShape(); + if (!shape) return; + this.setDrawnLatLngs(shape[0] || shape); // Polygon or polyline + this.startDrawingForward(); + // 🍂namespace Editable + // 🍂section Shape events + // 🍂event editable:shape:new: ShapeEvent + // Fired when a new shape is created in a multi (Polygon or Polyline). + this.fireAndForward('editable:shape:new', {shape: shape}); + if (latlng) this.newPointForward(latlng); + }, + + deleteShape: function (shape, latlngs) { + var e = {shape: shape}; + L.Editable.makeCancellable(e); + // 🍂namespace Editable + // 🍂section Shape events + // 🍂event editable:shape:delete: CancelableShapeEvent + // Fired before a new shape is deleted in a multi (Polygon or Polyline). + this.fireAndForward('editable:shape:delete', e); + if (e._cancelled) return; + shape = this._deleteShape(shape, latlngs); + if (this.ensureNotFlat) this.ensureNotFlat(); // Polygon. + this.feature.setLatLngs(this.getLatLngs()); // Force bounds reset. + this.refresh(); + this.reset(); + // 🍂namespace Editable + // 🍂section Shape events + // 🍂event editable:shape:deleted: ShapeEvent + // Fired after a new shape is deleted in a multi (Polygon or Polyline). + this.fireAndForward('editable:shape:deleted', {shape: shape}); + return shape; + }, + + _deleteShape: function (shape, latlngs) { + latlngs = latlngs || this.getLatLngs(); + if (!latlngs.length) return; + var self = this, + inplaceDelete = function (latlngs, shape) { + // Called when deleting a flat latlngs + shape = latlngs.splice(0, Number.MAX_VALUE); + return shape; + }, + spliceDelete = function (latlngs, shape) { + // Called when removing a latlngs inside an array + latlngs.splice(latlngs.indexOf(shape), 1); + if (!latlngs.length) self._deleteShape(latlngs); + return shape; + }; + if (latlngs === shape) return inplaceDelete(latlngs, shape); + for (var i = 0; i < latlngs.length; i++) { + if (latlngs[i] === shape) return spliceDelete(latlngs, shape); + else if (latlngs[i].indexOf(shape) !== -1) return spliceDelete(latlngs[i], shape); + } + }, + + // 🍂namespace PathEditor + // 🍂method deleteShapeAt(latlng: L.LatLng): Array + // Remove a path shape at the given `latlng`. + deleteShapeAt: function (latlng) { + var shape = this.feature.shapeAt(latlng); + if (shape) return this.deleteShape(shape); + }, + + // 🍂method appendShape(shape: Array) + // Append a new shape to the Polygon or Polyline. + appendShape: function (shape) { + this.insertShape(shape); + }, + + // 🍂method prependShape(shape: Array) + // Prepend a new shape to the Polygon or Polyline. + prependShape: function (shape) { + this.insertShape(shape, 0); + }, + + // 🍂method insertShape(shape: Array, index: int) + // Insert a new shape to the Polygon or Polyline at given index (default is to append). + insertShape: function (shape, index) { + this.ensureMulti(); + shape = this.formatShape(shape); + if (typeof index === 'undefined') index = this.feature._latlngs.length; + this.feature._latlngs.splice(index, 0, shape); + this.feature.redraw(); + if (this._enabled) this.reset(); + }, + + extendBounds: function (e) { + this.feature._bounds.extend(e.vertex.latlng); + }, + + onDragStart: function (e) { + this.editLayer.clearLayers(); + L.Editable.BaseEditor.prototype.onDragStart.call(this, e); + }, + + onDragEnd: function (e) { + this.initVertexMarkers(); + L.Editable.BaseEditor.prototype.onDragEnd.call(this, e); + } + + }); + + // 🍂namespace Editable; 🍂class PolylineEditor; 🍂aka L.Editable.PolylineEditor + // 🍂inherits PathEditor + L.Editable.PolylineEditor = L.Editable.PathEditor.extend({ + + startDrawingBackward: function () { + this._drawing = L.Editable.BACKWARD; + this.startDrawing(); + }, + + // 🍂method continueBackward(latlngs?: Array) + // Set up drawing tools to continue the line backward. + continueBackward: function (latlngs) { + if (this.drawing()) return; + latlngs = latlngs || this.getDefaultLatLngs(); + this.setDrawnLatLngs(latlngs); + if (latlngs.length > 0) { + this.tools.attachBackwardLineGuide(); + this.tools.anchorBackwardLineGuide(latlngs[0]); + } + this.startDrawingBackward(); + }, + + // 🍂method continueForward(latlngs?: Array) + // Set up drawing tools to continue the line forward. + continueForward: function (latlngs) { + if (this.drawing()) return; + latlngs = latlngs || this.getDefaultLatLngs(); + this.setDrawnLatLngs(latlngs); + if (latlngs.length > 0) { + this.tools.attachForwardLineGuide(); + this.tools.anchorForwardLineGuide(latlngs[latlngs.length - 1]); + } + this.startDrawingForward(); + }, + + getDefaultLatLngs: function (latlngs) { + latlngs = latlngs || this.feature._latlngs; + if (!latlngs.length || latlngs[0] instanceof L.LatLng) return latlngs; + else return this.getDefaultLatLngs(latlngs[0]); + }, + + ensureMulti: function () { + if (this.feature._latlngs.length && isFlat(this.feature._latlngs)) { + this.feature._latlngs = [this.feature._latlngs]; + } + }, + + addNewEmptyShape: function () { + if (this.feature._latlngs.length) { + var shape = []; + this.appendShape(shape); + return shape; + } else { + return this.feature._latlngs; + } + }, + + formatShape: function (shape) { + if (isFlat(shape)) return shape; + else if (shape[0]) return this.formatShape(shape[0]); + }, + + // 🍂method splitShape(latlngs?: Array, index: int) + // Split the given `latlngs` shape at index `index` and integrate new shape in instance `latlngs`. + splitShape: function (shape, index) { + if (!index || index >= shape.length - 1) return; + this.ensureMulti(); + var shapeIndex = this.feature._latlngs.indexOf(shape); + if (shapeIndex === -1) return; + var first = shape.slice(0, index + 1), + second = shape.slice(index); + // We deal with reference, we don't want twice the same latlng around. + second[0] = L.latLng(second[0].lat, second[0].lng, second[0].alt); + this.feature._latlngs.splice(shapeIndex, 1, first, second); + this.refresh(); + this.reset(); + } + + }); + + // 🍂namespace Editable; 🍂class PolygonEditor; 🍂aka L.Editable.PolygonEditor + // 🍂inherits PathEditor + L.Editable.PolygonEditor = L.Editable.PathEditor.extend({ + + CLOSED: true, + MIN_VERTEX: 3, + + newPointForward: function (latlng) { + L.Editable.PathEditor.prototype.newPointForward.call(this, latlng); + if (!this.tools.backwardLineGuide._latlngs.length) this.tools.anchorBackwardLineGuide(latlng); + if (this._drawnLatLngs.length === 2) this.tools.attachBackwardLineGuide(); + }, + + addNewEmptyHole: function (latlng) { + this.ensureNotFlat(); + var latlngs = this.feature.shapeAt(latlng); + if (!latlngs) return; + var holes = []; + latlngs.push(holes); + return holes; + }, + + // 🍂method newHole(latlng?: L.LatLng, index: int) + // Set up drawing tools for creating a new hole on the Polygon. If the `latlng` param is given, a first point is created. + newHole: function (latlng) { + var holes = this.addNewEmptyHole(latlng); + if (!holes) return; + this.setDrawnLatLngs(holes); + this.startDrawingForward(); + if (latlng) this.newPointForward(latlng); + }, + + addNewEmptyShape: function () { + if (this.feature._latlngs.length && this.feature._latlngs[0].length) { + var shape = []; + this.appendShape(shape); + return shape; + } else { + return this.feature._latlngs; + } + }, + + ensureMulti: function () { + if (this.feature._latlngs.length && isFlat(this.feature._latlngs[0])) { + this.feature._latlngs = [this.feature._latlngs]; + } + }, + + ensureNotFlat: function () { + if (!this.feature._latlngs.length || isFlat(this.feature._latlngs)) this.feature._latlngs = [this.feature._latlngs]; + }, + + vertexCanBeDeleted: function (vertex) { + var parent = this.feature.parentShape(vertex.latlngs), + idx = L.Util.indexOf(parent, vertex.latlngs); + if (idx > 0) return true; // Holes can be totally deleted without removing the layer itself. + return L.Editable.PathEditor.prototype.vertexCanBeDeleted.call(this, vertex); + }, + + getDefaultLatLngs: function () { + if (!this.feature._latlngs.length) this.feature._latlngs.push([]); + return this.feature._latlngs[0]; + }, + + formatShape: function (shape) { + // [[1, 2], [3, 4]] => must be nested + // [] => must be nested + // [[]] => is already nested + if (isFlat(shape) && (!shape[0] || shape[0].length !== 0)) return [shape]; + else return shape; + } + + }); + + // 🍂namespace Editable; 🍂class RectangleEditor; 🍂aka L.Editable.RectangleEditor + // 🍂inherits PathEditor + L.Editable.RectangleEditor = L.Editable.PathEditor.extend({ + + CLOSED: true, + MIN_VERTEX: 4, + + options: { + skipMiddleMarkers: true + }, + + extendBounds: function (e) { + var index = e.vertex.getIndex(), + next = e.vertex.getNext(), + previous = e.vertex.getPrevious(), + oppositeIndex = (index + 2) % 4, + opposite = e.vertex.latlngs[oppositeIndex], + bounds = new L.LatLngBounds(e.latlng, opposite); + // Update latlngs by hand to preserve order. + previous.latlng.update([e.latlng.lat, opposite.lng]); + next.latlng.update([opposite.lat, e.latlng.lng]); + this.updateBounds(bounds); + this.refreshVertexMarkers(); + }, + + onDrawingMouseDown: function (e) { + L.Editable.PathEditor.prototype.onDrawingMouseDown.call(this, e); + this.connect(); + var latlngs = this.getDefaultLatLngs(); + // L.Polygon._convertLatLngs removes last latlng if it equals first point, + // which is the case here as all latlngs are [0, 0] + if (latlngs.length === 3) latlngs.push(e.latlng); + var bounds = new L.LatLngBounds(e.latlng, e.latlng); + this.updateBounds(bounds); + this.updateLatLngs(bounds); + this.refresh(); + this.reset(); + // Stop dragging map. + // L.Draggable has two workflows: + // - mousedown => mousemove => mouseup + // - touchstart => touchmove => touchend + // Problem: L.Map.Tap does not allow us to listen to touchstart, so we only + // can deal with mousedown, but then when in a touch device, we are dealing with + // simulated events (actually simulated by L.Map.Tap), which are no more taken + // into account by L.Draggable. + // Ref.: https://github.com/Leaflet/Leaflet.Editable/issues/103 + e.originalEvent._simulated = false; + this.map.dragging._draggable._onUp(e.originalEvent); + // Now transfer ongoing drag action to the bottom right corner. + // Should we refine which corner will handle the drag according to + // drag direction? + latlngs[3].__vertex.dragging._draggable._onDown(e.originalEvent); + }, + + onDrawingMouseUp: function (e) { + this.commitDrawing(e); + e.originalEvent._simulated = false; + L.Editable.PathEditor.prototype.onDrawingMouseUp.call(this, e); + }, + + onDrawingMouseMove: function (e) { + e.originalEvent._simulated = false; + L.Editable.PathEditor.prototype.onDrawingMouseMove.call(this, e); + }, + + + getDefaultLatLngs: function (latlngs) { + return latlngs || this.feature._latlngs[0]; + }, + + updateBounds: function (bounds) { + this.feature._bounds = bounds; + }, + + updateLatLngs: function (bounds) { + var latlngs = this.getDefaultLatLngs(), + newLatlngs = this.feature._boundsToLatLngs(bounds); + // Keep references. + for (var i = 0; i < latlngs.length; i++) { + latlngs[i].update(newLatlngs[i]); + } + } + + }); + + // 🍂namespace Editable; 🍂class CircleEditor; 🍂aka L.Editable.CircleEditor + // 🍂inherits PathEditor + L.Editable.CircleEditor = L.Editable.PathEditor.extend({ + + MIN_VERTEX: 2, + + options: { + skipMiddleMarkers: true + }, + + initialize: function (map, feature, options) { + L.Editable.PathEditor.prototype.initialize.call(this, map, feature, options); + this._resizeLatLng = this.computeResizeLatLng(); + }, + + computeResizeLatLng: function () { + // While circle is not added to the map, _radius is not set. + var delta = (this.feature._radius || this.feature._mRadius) * Math.cos(Math.PI / 4), + point = this.map.project(this.feature._latlng); + return this.map.unproject([point.x + delta, point.y - delta]); + }, + + updateResizeLatLng: function () { + this._resizeLatLng.update(this.computeResizeLatLng()); + this._resizeLatLng.__vertex.update(); + }, + + getLatLngs: function () { + return [this.feature._latlng, this._resizeLatLng]; + }, + + getDefaultLatLngs: function () { + return this.getLatLngs(); + }, + + onVertexMarkerDrag: function (e) { + if (e.vertex.getIndex() === 1) this.resize(e); + else this.updateResizeLatLng(e); + L.Editable.PathEditor.prototype.onVertexMarkerDrag.call(this, e); + }, + + resize: function (e) { + var radius = this.feature._latlng.distanceTo(e.latlng); + this.feature.setRadius(radius); + }, + + onDrawingMouseDown: function (e) { + L.Editable.PathEditor.prototype.onDrawingMouseDown.call(this, e); + this._resizeLatLng.update(e.latlng); + this.feature._latlng.update(e.latlng); + this.connect(); + // Stop dragging map. + e.originalEvent._simulated = false; + this.map.dragging._draggable._onUp(e.originalEvent); + // Now transfer ongoing drag action to the radius handler. + this._resizeLatLng.__vertex.dragging._draggable._onDown(e.originalEvent); + }, + + onDrawingMouseUp: function (e) { + this.commitDrawing(e); + e.originalEvent._simulated = false; + L.Editable.PathEditor.prototype.onDrawingMouseUp.call(this, e); + }, + + onDrawingMouseMove: function (e) { + e.originalEvent._simulated = false; + L.Editable.PathEditor.prototype.onDrawingMouseMove.call(this, e); + }, + + onDrag: function (e) { + L.Editable.PathEditor.prototype.onDrag.call(this, e); + this.feature.dragging.updateLatLng(this._resizeLatLng); + } + + }); + + // 🍂namespace Editable; 🍂class EditableMixin + // `EditableMixin` is included to `L.Polyline`, `L.Polygon`, `L.Rectangle`, `L.Circle` + // and `L.Marker`. It adds some methods to them. + // *When editing is enabled, the editor is accessible on the instance with the + // `editor` property.* + var EditableMixin = { + + createEditor: function (map) { + map = map || this._map; + var tools = (this.options.editOptions || {}).editTools || map.editTools; + if (!tools) throw Error('Unable to detect Editable instance.'); + var Klass = this.options.editorClass || this.getEditorClass(tools); + return new Klass(map, this, this.options.editOptions); + }, + + // 🍂method enableEdit(map?: L.Map): this.editor + // Enable editing, by creating an editor if not existing, and then calling `enable` on it. + enableEdit: function (map) { + if (!this.editor) this.createEditor(map); + this.editor.enable(); + return this.editor; + }, + + // 🍂method editEnabled(): boolean + // Return true if current instance has an editor attached, and this editor is enabled. + editEnabled: function () { + return this.editor && this.editor.enabled(); + }, + + // 🍂method disableEdit() + // Disable editing, also remove the editor property reference. + disableEdit: function () { + if (this.editor) { + this.editor.disable(); + delete this.editor; + } + }, + + // 🍂method toggleEdit() + // Enable or disable editing, according to current status. + toggleEdit: function () { + if (this.editEnabled()) this.disableEdit(); + else this.enableEdit(); + }, + + _onEditableAdd: function () { + if (this.editor) this.enableEdit(); + } + + }; + + var PolylineMixin = { + + getEditorClass: function (tools) { + return (tools && tools.options.polylineEditorClass) ? tools.options.polylineEditorClass : L.Editable.PolylineEditor; + }, + + shapeAt: function (latlng, latlngs) { + // We can have those cases: + // - latlngs are just a flat array of latlngs, use this + // - latlngs is an array of arrays of latlngs, loop over + var shape = null; + latlngs = latlngs || this._latlngs; + if (!latlngs.length) return shape; + else if (isFlat(latlngs) && this.isInLatLngs(latlng, latlngs)) shape = latlngs; + else for (var i = 0; i < latlngs.length; i++) if (this.isInLatLngs(latlng, latlngs[i])) return latlngs[i]; + return shape; + }, + + isInLatLngs: function (l, latlngs) { + if (!latlngs) return false; + var i, k, len, part = [], p, + w = this._clickTolerance(); + this._projectLatlngs(latlngs, part, this._pxBounds); + part = part[0]; + p = this._map.latLngToLayerPoint(l); + + if (!this._pxBounds.contains(p)) { return false; } + for (i = 1, len = part.length, k = 0; i < len; k = i++) { + + if (L.LineUtil.pointToSegmentDistance(p, part[k], part[i]) <= w) { + return true; + } + } + return false; + } + + }; + + var PolygonMixin = { + + getEditorClass: function (tools) { + return (tools && tools.options.polygonEditorClass) ? tools.options.polygonEditorClass : L.Editable.PolygonEditor; + }, + + shapeAt: function (latlng, latlngs) { + // We can have those cases: + // - latlngs are just a flat array of latlngs, use this + // - latlngs is an array of arrays of latlngs, this is a simple polygon (maybe with holes), use the first + // - latlngs is an array of arrays of arrays, this is a multi, loop over + var shape = null; + latlngs = latlngs || this._latlngs; + if (!latlngs.length) return shape; + else if (isFlat(latlngs) && this.isInLatLngs(latlng, latlngs)) shape = latlngs; + else if (isFlat(latlngs[0]) && this.isInLatLngs(latlng, latlngs[0])) shape = latlngs; + else for (var i = 0; i < latlngs.length; i++) if (this.isInLatLngs(latlng, latlngs[i][0])) return latlngs[i]; + return shape; + }, + + isInLatLngs: function (l, latlngs) { + var inside = false, l1, l2, j, k, len2; + + for (j = 0, len2 = latlngs.length, k = len2 - 1; j < len2; k = j++) { + l1 = latlngs[j]; + l2 = latlngs[k]; + + if (((l1.lat > l.lat) !== (l2.lat > l.lat)) && + (l.lng < (l2.lng - l1.lng) * (l.lat - l1.lat) / (l2.lat - l1.lat) + l1.lng)) { + inside = !inside; + } + } + + return inside; + }, + + parentShape: function (shape, latlngs) { + latlngs = latlngs || this._latlngs; + if (!latlngs) return; + var idx = L.Util.indexOf(latlngs, shape); + if (idx !== -1) return latlngs; + for (var i = 0; i < latlngs.length; i++) { + idx = L.Util.indexOf(latlngs[i], shape); + if (idx !== -1) return latlngs[i]; + } + } + + }; + + + var MarkerMixin = { + + getEditorClass: function (tools) { + return (tools && tools.options.markerEditorClass) ? tools.options.markerEditorClass : L.Editable.MarkerEditor; + } + + }; + + var RectangleMixin = { + + getEditorClass: function (tools) { + return (tools && tools.options.rectangleEditorClass) ? tools.options.rectangleEditorClass : L.Editable.RectangleEditor; + } + + }; + + var CircleMixin = { + + getEditorClass: function (tools) { + return (tools && tools.options.circleEditorClass) ? tools.options.circleEditorClass : L.Editable.CircleEditor; + } + + }; + + var keepEditable = function () { + // Make sure you can remove/readd an editable layer. + this.on('add', this._onEditableAdd); + }; + + var isFlat = L.LineUtil.isFlat || L.LineUtil._flat || L.Polyline._flat; // <=> 1.1 compat. + + + if (L.Polyline) { + L.Polyline.include(EditableMixin); + L.Polyline.include(PolylineMixin); + L.Polyline.addInitHook(keepEditable); + } + if (L.Polygon) { + L.Polygon.include(EditableMixin); + L.Polygon.include(PolygonMixin); + } + if (L.Marker) { + L.Marker.include(EditableMixin); + L.Marker.include(MarkerMixin); + L.Marker.addInitHook(keepEditable); + } + if (L.Rectangle) { + L.Rectangle.include(EditableMixin); + L.Rectangle.include(RectangleMixin); + } + if (L.Circle) { + L.Circle.include(EditableMixin); + L.Circle.include(CircleMixin); + } + + L.LatLng.prototype.update = function (latlng) { + latlng = L.latLng(latlng); + this.lat = latlng.lat; + this.lng = latlng.lng; + } + +}, window)); From 96e6e420380f9b3034482a469f725d7725600956 Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Sun, 11 Dec 2022 14:57:59 +0100 Subject: [PATCH 12/29] Update tests --- CHANGES.rst | 3 +++ src/cone/maps/tests.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f3b3999..005813d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,9 @@ Changes 0.2 (unreleased) ---------------- +- Add ``Leaflet.Editable`` to resources. + [rnix] + - Map settings are defined via ``MapTile.map_settings`` property. [rnix] diff --git a/src/cone/maps/tests.py b/src/cone/maps/tests.py index 34d2a39..4978a4e 100644 --- a/src/cone/maps/tests.py +++ b/src/cone/maps/tests.py @@ -141,6 +141,23 @@ def test_leaflet_markercluster_resources(self): self.assertEqual(styles[1].file_name, 'MarkerCluster.Default.css') self.assertTrue(os.path.exists(styles[1].file_path)) + def test_leaflet_editable_resources(self): + resources_ = browser.leaflet_editable_resources + self.assertTrue(resources_.directory.endswith(np('/static/leaflet-editable'))) + self.assertEqual(resources_.name, 'cone.maps-leaflet-editable') + self.assertEqual(resources_.path, 'leaflet-editable') + + scripts = resources_.scripts + self.assertEqual(len(scripts), 1) + + self.assertTrue(scripts[0].directory.endswith(np('/static/leaflet-editable'))) + self.assertEqual(scripts[0].path, 'leaflet-editable') + self.assertEqual(scripts[0].file_name, 'Leaflet.Editable.js') + self.assertTrue(os.path.exists(scripts[0].file_path)) + + styles = resources_.styles + self.assertEqual(len(styles), 0) + def test_leaflet_activearea_resources(self): resources_ = browser.leaflet_activearea_resources self.assertTrue(resources_.directory.endswith(np('/static/leaflet-activearea'))) @@ -229,6 +246,7 @@ def set_resource_include(self, name, value): self.assertEqual(config.includes, { 'leaflet-js': 'authenticated', 'leaflet-css': 'authenticated', + 'leaflet-editable-js': False, 'leaflet-nogap-js': False, 'leaflet-geosearch-js': False, 'leaflet-geosearch-css': False, @@ -247,6 +265,7 @@ def set_resource_include(self, name, value): 'cone.maps.nogap': 'true', 'cone.maps.geosearch': 'true', 'cone.maps.markercluster': 'true', + 'cone.maps.editable': 'true', 'cone.maps.activearea': 'true', 'cone.maps.proj4': 'true' } @@ -254,6 +273,7 @@ def set_resource_include(self, name, value): self.assertEqual(config.includes, { 'leaflet-js': 'authenticated', 'leaflet-css': 'authenticated', + 'leaflet-editable-js': 'authenticated', 'leaflet-nogap-js': 'authenticated', 'leaflet-geosearch-js': 'authenticated', 'leaflet-geosearch-css': 'authenticated', @@ -272,6 +292,7 @@ def set_resource_include(self, name, value): 'cone.maps.nogap': 'true', 'cone.maps.geosearch': 'true', 'cone.maps.markercluster': 'true', + 'cone.maps.editable': 'true', 'cone.maps.activearea': 'true', 'cone.maps.proj4': 'true' } @@ -279,6 +300,7 @@ def set_resource_include(self, name, value): self.assertEqual(config.includes, { 'leaflet-js': True, 'leaflet-css': True, + 'leaflet-editable-js': True, 'leaflet-nogap-js': True, 'leaflet-geosearch-js': True, 'leaflet-geosearch-css': True, From c07f81794d408d8a13893ce64639f1f9e0e062cb Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Wed, 8 Feb 2023 20:02:09 +0100 Subject: [PATCH 13/29] Add ``Path.Drag.js`` to resources. --- CHANGES.rst | 3 + README.rst | 13 +- src/cone/maps/browser/__init__.py | 19 ++- .../static/leaflet-pathdrag/Path.Drag.js | 138 ++++++++++++++++++ 4 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 src/cone/maps/browser/static/leaflet-pathdrag/Path.Drag.js diff --git a/CHANGES.rst b/CHANGES.rst index 005813d..d70b4d8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,9 @@ Changes 0.2 (unreleased) ---------------- +- Add ``Path.Drag.js`` to resources. + [rnix] + - Add ``Leaflet.Editable`` to resources. [rnix] diff --git a/README.rst b/README.rst index df1d8f5..48177b6 100644 --- a/README.rst +++ b/README.rst @@ -30,10 +30,14 @@ This package provides maps integration in to cone.app. `Leaflet.markercluster `_ (1.5.3) is included. -* Make meking geometries editable in Leaflet, +* For making geometries editable in Leaflet, `Leaflet.Editable `_ (1.2.0) is included. +* For adding dragging capability to Leaflet paths, + `Path.Drag.js `_ + (0.0.6) is included. + * For defining active map area, e.g. if parts of a map is used as background, `Leaflet-active-area `_ (1.2.0) is included. @@ -83,8 +87,11 @@ available : - **cone.maps.markercluster**: Flag whether to include ``Leaflet.markercluster`` plugin. Defaults to `false`. -- **cone.maps.editable**: Flag whether to include ``Leaflet.Editable`` - plugin. Defaults to `false`. +- **cone.maps.editable**: Flag whether to include ``Leaflet.Editable`` plugin. + Defaults to `false`. + +- **cone.maps.pathdrag**: Flag whether to include ``Path.Drag.js`` plugin. + Defaults to `false`. - **cone.maps.activearea**: Flag whether to include ``Leaflet-active-area`` plugin. Defaults to `false`. diff --git a/src/cone/maps/browser/__init__.py b/src/cone/maps/browser/__init__.py index cb182c7..3d9cbb0 100644 --- a/src/cone/maps/browser/__init__.py +++ b/src/cone/maps/browser/__init__.py @@ -85,6 +85,18 @@ resource='Leaflet.Editable.js' )) +# Leaflet Path.Drag.js +leaflet_pathdrag_resources = wr.ResourceGroup( + name='cone.maps-leaflet-pathdrag', + directory=os.path.join(resources_dir, 'leaflet-pathdrag'), + path='leaflet-pathdrag' +) +leaflet_pathdrag_resources.add(wr.ScriptResource( + name='leaflet-pathdrag-js', + depends='leaflet-js', + resource='Path.Drag.js' +)) + # Leaflet-active-area leaflet_activearea_resources = wr.ResourceGroup( name='cone.maps-leaflet-activearea', @@ -141,7 +153,7 @@ def included(name): include = True if included('cone.maps.public') else 'authenticated' - # leaflet core + # Leaflet core config.register_resource(leaflet_resources) config.set_resource_include('leaflet-js', include) config.set_resource_include('leaflet-css', include) @@ -169,6 +181,11 @@ def included(name): editable_include = include if included('cone.maps.editable') else False config.set_resource_include('leaflet-editable-js', editable_include) + # Leaflet Path.Drag.js + config.register_resource(leaflet_pathdrag_resources) + pathdrag_include = include if included('cone.maps.pathdrag') else False + config.set_resource_include('leaflet-pathdrag-js', pathdrag_include) + # Leaflet-active-area config.register_resource(leaflet_activearea_resources) activearea_include = include if included('cone.maps.activearea') else False diff --git a/src/cone/maps/browser/static/leaflet-pathdrag/Path.Drag.js b/src/cone/maps/browser/static/leaflet-pathdrag/Path.Drag.js new file mode 100644 index 0000000..6c64432 --- /dev/null +++ b/src/cone/maps/browser/static/leaflet-pathdrag/Path.Drag.js @@ -0,0 +1,138 @@ +'use strict'; + +/* A Draggable that does not update the element position +and takes care of only bubbling to targetted path in Canvas mode. */ +L.PathDraggable = L.Draggable.extend({ + + initialize: function (path) { + this._path = path; + this._canvas = (path._map.getRenderer(path) instanceof L.Canvas); + var element = this._canvas ? this._path._map.getRenderer(this._path)._container : this._path._path; + L.Draggable.prototype.initialize.call(this, element, element, true); + }, + + _updatePosition: function () { + var e = {originalEvent: this._lastEvent}; + this.fire('drag', e); + }, + + _onDown: function (e) { + var first = e.touches ? e.touches[0] : e; + this._startPoint = new L.Point(first.clientX, first.clientY); + if (this._canvas && !this._path._containsPoint(this._path._map.mouseEventToLayerPoint(first))) { return; } + L.Draggable.prototype._onDown.call(this, e); + } + +}); + + +L.Handler.PathDrag = L.Handler.extend({ + + initialize: function (path) { + this._path = path; + }, + + getEvents: function () { + return { + dragstart: this._onDragStart, + drag: this._onDrag, + dragend: this._onDragEnd + }; + }, + + addHooks: function () { + if (!this._draggable) { this._draggable = new L.PathDraggable(this._path); } + this._draggable.on(this.getEvents(), this).enable(); + L.DomUtil.addClass(this._draggable._element, 'leaflet-path-draggable'); + }, + + removeHooks: function () { + this._draggable.off(this.getEvents(), this).disable(); + L.DomUtil.removeClass(this._draggable._element, 'leaflet-path-draggable'); + }, + + moved: function () { + return this._draggable && this._draggable._moved; + }, + + _onDragStart: function () { + this._startPoint = this._draggable._startPoint; + this._path + .closePopup() + .fire('movestart') + .fire('dragstart'); + }, + + _onDrag: function (e) { + var path = this._path, + event = (e.originalEvent.touches && e.originalEvent.touches.length === 1 ? e.originalEvent.touches[0] : e.originalEvent), + newPoint = L.point(event.clientX, event.clientY), + latlng = path._map.layerPointToLatLng(newPoint); + + this._offset = newPoint.subtract(this._startPoint); + this._startPoint = newPoint; + + this._path.eachLatLng(this.updateLatLng, this); + path.redraw(); + + e.latlng = latlng; + e.offset = this._offset; + path.fire('drag', e); + e.latlng = this._path.getCenter ? this._path.getCenter() : this._path.getLatLng(); + path.fire('move', e); + }, + + _onDragEnd: function (e) { + if (this._path._bounds) this.resetBounds(); + this._path.fire('moveend') + .fire('dragend', e); + }, + + latLngToLayerPoint: function (latlng) { + // Same as map.latLngToLayerPoint, but without the round(). + var projectedPoint = this._path._map.project(L.latLng(latlng)); + return projectedPoint._subtract(this._path._map.getPixelOrigin()); + }, + + updateLatLng: function (latlng) { + var oldPoint = this.latLngToLayerPoint(latlng); + oldPoint._add(this._offset); + var newLatLng = this._path._map.layerPointToLatLng(oldPoint); + latlng.lat = newLatLng.lat; + latlng.lng = newLatLng.lng; + }, + + resetBounds: function () { + this._path._bounds = new L.LatLngBounds(); + this._path.eachLatLng(function (latlng) { + this._bounds.extend(latlng); + }); + } + +}); + +L.Path.include({ + + eachLatLng: function (callback, context) { + context = context || this; + var loop = function (latlngs) { + for (var i = 0; i < latlngs.length; i++) { + if (L.Util.isArray(latlngs[i])) loop(latlngs[i]); + else callback.call(context, latlngs[i]); + } + }; + loop(this.getLatLngs ? this.getLatLngs() : [this.getLatLng()]); + } + +}); + +L.Path.addInitHook(function () { + + this.dragging = new L.Handler.PathDrag(this); + if (this.options.draggable) { + this.once('add', function () { + this.dragging.enable(); + }); + } + +}); From 21f6f26ea07f26194c8b508f2fcaef40d701b30e Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Wed, 1 Mar 2023 18:16:26 +0100 Subject: [PATCH 14/29] Fix tests --- src/cone/maps/tests.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cone/maps/tests.py b/src/cone/maps/tests.py index 4978a4e..a45d9d2 100644 --- a/src/cone/maps/tests.py +++ b/src/cone/maps/tests.py @@ -247,6 +247,7 @@ def set_resource_include(self, name, value): 'leaflet-js': 'authenticated', 'leaflet-css': 'authenticated', 'leaflet-editable-js': False, + 'leaflet-pathdrag-js': False, 'leaflet-nogap-js': False, 'leaflet-geosearch-js': False, 'leaflet-geosearch-css': False, @@ -266,6 +267,7 @@ def set_resource_include(self, name, value): 'cone.maps.geosearch': 'true', 'cone.maps.markercluster': 'true', 'cone.maps.editable': 'true', + 'cone.maps.pathdrag': 'true', 'cone.maps.activearea': 'true', 'cone.maps.proj4': 'true' } @@ -274,6 +276,7 @@ def set_resource_include(self, name, value): 'leaflet-js': 'authenticated', 'leaflet-css': 'authenticated', 'leaflet-editable-js': 'authenticated', + 'leaflet-pathdrag-js': 'authenticated', 'leaflet-nogap-js': 'authenticated', 'leaflet-geosearch-js': 'authenticated', 'leaflet-geosearch-css': 'authenticated', @@ -293,6 +296,7 @@ def set_resource_include(self, name, value): 'cone.maps.geosearch': 'true', 'cone.maps.markercluster': 'true', 'cone.maps.editable': 'true', + 'cone.maps.pathdrag': 'true', 'cone.maps.activearea': 'true', 'cone.maps.proj4': 'true' } @@ -301,6 +305,7 @@ def set_resource_include(self, name, value): 'leaflet-js': True, 'leaflet-css': True, 'leaflet-editable-js': True, + 'leaflet-pathdrag-js': True, 'leaflet-nogap-js': True, 'leaflet-geosearch-js': True, 'leaflet-geosearch-css': True, From 5894a39174dbb54119a32bfc91e8f896ca22c513 Mon Sep 17 00:00:00 2001 From: Lena Daxenbichler Date: Fri, 15 Nov 2024 12:52:24 +0100 Subject: [PATCH 15/29] Makefile --- .gitignore | 7 + CHANGES.rst | 6 + Makefile | 745 ++++++++++++++++++ mx.ini | 93 +++ package.json | 3 +- setup.py | 18 +- .../maps/{tests.py => tests/test_package.py} | 0 7 files changed, 859 insertions(+), 13 deletions(-) create mode 100644 Makefile create mode 100644 mx.ini rename src/cone/maps/{tests.py => tests/test_package.py} (100%) diff --git a/.gitignore b/.gitignore index a8ae82e..d929231 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,10 @@ __pycache__ /src/cone.maps.egg-info/ /node_modules/ /package-lock.json +.coverage +/.mxmake/ +/venv/ +/.venv/ +/requirements-mxdev.txt +/sources/ +/constraints-mxdev.txt diff --git a/CHANGES.rst b/CHANGES.rst index d70b4d8..be4463a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,12 @@ Changes 0.2 (unreleased) ---------------- +- Setup Makefile. + [lenadax] + +- Run tests with pytest. + [lenadax] + - Add ``Path.Drag.js`` to resources. [rnix] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..abb8f3f --- /dev/null +++ b/Makefile @@ -0,0 +1,745 @@ +############################################################################## +# THIS FILE IS GENERATED BY MXMAKE +# +# DOMAINS: +#: applications.zest-releaser +#: core.base +#: core.mxenv +#: core.mxfiles +#: core.packages +#: core.sources +#: i18n.gettext +#: i18n.lingua +#: js.nodejs +#: js.rollup +#: qa.coverage +#: qa.test +# +# SETTINGS (ALL CHANGES MADE BELOW SETTINGS WILL BE LOST) +############################################################################## + +## core.base + +# `deploy` target dependencies. +# No default value. +DEPLOY_TARGETS?= + +# target to be executed when calling `make run` +# No default value. +RUN_TARGET?= + +# Additional files and folders to remove when running clean target +# No default value. +CLEAN_FS?= + +# Optional makefile to include before default targets. This can +# be used to provide custom targets or hook up to existing targets. +# Default: include.mk +INCLUDE_MAKEFILE?=include.mk + +# Optional additional directories to be added to PATH in format +# `/path/to/dir/:/path/to/other/dir`. Gets inserted first, thus gets searched +# first. +# No default value. +EXTRA_PATH?= + +## js.nodejs + +# The package manager to use. Defaults to `npm`. Possible values +# are `npm` and `pnpm` +# Default: npm +NODEJS_PACKAGE_MANAGER?=npm + +# Value for `--prefix` option when installing packages. +# Default: . +NODEJS_PREFIX?=. + +# Packages to install with `--no-save` option. +# No default value. +NODEJS_PACKAGES?= + +# Packages to install with `--save-dev` option. +# No default value. +NODEJS_DEV_PACKAGES?= + +# Packages to install with `--save-prod` option. +# No default value. +NODEJS_PROD_PACKAGES?= + +# Packages to install with `--save-optional` option. +# No default value. +NODEJS_OPT_PACKAGES?= + +# Additional install options. Possible values are `--save-exact` +# and `--save-bundle`. +# No default value. +NODEJS_INSTALL_OPTS?= + +## js.rollup + +# Rollup config file. +# Default: rollup.conf.js +ROLLUP_CONFIG?=js/rollup.conf.js + +## core.mxenv + +# Primary Python interpreter to use. It is used to create the +# virtual environment if `VENV_ENABLED` and `VENV_CREATE` are set to `true`. +# Default: python3 +PRIMARY_PYTHON?=python3 + +# Minimum required Python version. +# Default: 3.9 +PYTHON_MIN_VERSION?=3.9 + +# Install packages using the given package installer method. +# Supported are `pip` and `uv`. If uv is used, its global availability is +# checked. Otherwise, it is installed, either in the virtual environment or +# using the `PRIMARY_PYTHON`, dependent on the `VENV_ENABLED` setting. If +# `VENV_ENABLED` and uv is selected, uv is used to create the virtual +# environment. +# Default: pip +PYTHON_PACKAGE_INSTALLER?=pip + +# Flag whether to use a global installed 'uv' or install +# it in the virtual environment. +# Default: false +MXENV_UV_GLOBAL?=false + +# Flag whether to use virtual environment. If `false`, the +# interpreter according to `PRIMARY_PYTHON` found in `PATH` is used. +# Default: true +VENV_ENABLED?=true + +# Flag whether to create a virtual environment. If set to `false` +# and `VENV_ENABLED` is `true`, `VENV_FOLDER` is expected to point to an +# existing virtual environment. +# Default: true +VENV_CREATE?=true + +# The folder of the virtual environment. +# If `VENV_ENABLED` is `true` and `VENV_CREATE` is true it is used as the +# target folder for the virtual environment. If `VENV_ENABLED` is `true` and +# `VENV_CREATE` is false it is expected to point to an existing virtual +# environment. If `VENV_ENABLED` is `false` it is ignored. +# Default: .venv +VENV_FOLDER?=.venv + +# mxdev to install in virtual environment. +# Default: mxdev +MXDEV?=mxdev + +# mxmake to install in virtual environment. +# Default: mxmake +MXMAKE?=mxmake + +## core.mxfiles + +# The config file to use. +# Default: mx.ini +PROJECT_CONFIG?=mx.ini + +## core.packages + +# Allow prerelease and development versions. +# By default, the package installer only finds stable versions. +# Default: false +PACKAGES_ALLOW_PRERELEASES?=false + +## qa.test + +# The command which gets executed. Defaults to the location the +# :ref:`run-tests` template gets rendered to if configured. +# Default: .mxmake/files/run-tests.sh +TEST_COMMAND?=.mxmake/files/run-tests.sh + +# Additional Python requirements for running tests to be +# installed (via pip). +# Default: pytest +TEST_REQUIREMENTS?=pytest + +# Additional make targets the test target depends on. +# No default value. +TEST_DEPENDENCY_TARGETS?= + +## qa.coverage + +# The command which gets executed. Defaults to the location the +# :ref:`run-coverage` template gets rendered to if configured. +# Default: .mxmake/files/run-coverage.sh +COVERAGE_COMMAND?=.mxmake/files/run-coverage.sh + +## applications.zest-releaser + +# Options to pass to zest.releaser prerelease command. +# No default value. +ZEST_RELEASER_PRERELEASE_OPTIONS?= + +# Options to pass to zest.releaser release command. +# No default value. +ZEST_RELEASER_RELEASE_OPTIONS?= + +# Options to pass to zest.releaser postrelease command. +# No default value. +ZEST_RELEASER_POSTRELEASE_OPTIONS?= + +# Options to pass to zest.releaser fullrelease command. +# No default value. +ZEST_RELEASER_FULLRELEASE_OPTIONS?= + +## i18n.gettext + +# Path of directory containing the message catalogs. +# Default: locale +GETTEXT_LOCALES_PATH?=locale + +# Translation domain to use. +# No default value. +GETTEXT_DOMAIN?= + +# Space separated list of language identifiers. +# No default value. +GETTEXT_LANGUAGES?= + +## i18n.lingua + +# Path of directory to extract translatable texts from. +# Default: src +LINGUA_SEARCH_PATH?=src + +# Python packages containing lingua extensions. +# No default value. +LINGUA_PLUGINS?= + +# Command line options passed to `pot-create` +# No default value. +LINGUA_OPTIONS?= + +############################################################################## +# END SETTINGS - DO NOT EDIT BELOW THIS LINE +############################################################################## + +INSTALL_TARGETS?= +DIRTY_TARGETS?= +CLEAN_TARGETS?= +PURGE_TARGETS?= +CHECK_TARGETS?= +TYPECHECK_TARGETS?= +FORMAT_TARGETS?= + +export PATH:=$(if $(EXTRA_PATH),$(EXTRA_PATH):,)$(PATH) + +# Defensive settings for make: https://tech.davis-hansson.com/p/make/ +SHELL:=bash +.ONESHELL: +# for Makefile debugging purposes add -x to the .SHELLFLAGS +.SHELLFLAGS:=-eu -o pipefail -O inherit_errexit -c +.SILENT: +.DELETE_ON_ERROR: +MAKEFLAGS+=--warn-undefined-variables +MAKEFLAGS+=--no-builtin-rules + +# mxmake folder +MXMAKE_FOLDER?=.mxmake + +# Sentinel files +SENTINEL_FOLDER?=$(MXMAKE_FOLDER)/sentinels +SENTINEL?=$(SENTINEL_FOLDER)/about.txt +$(SENTINEL): $(firstword $(MAKEFILE_LIST)) + @mkdir -p $(SENTINEL_FOLDER) + @echo "Sentinels for the Makefile process." > $(SENTINEL) + +############################################################################## +# nodejs +############################################################################## + +export PATH:=$(shell pwd)/$(NODEJS_PREFIX)/node_modules/.bin:$(PATH) + + +NODEJS_TARGET:=$(SENTINEL_FOLDER)/nodejs.sentinel +$(NODEJS_TARGET): $(SENTINEL) + @echo "Install nodejs packages" + @test -z "$(NODEJS_DEV_PACKAGES)" \ + && echo "No dev packages to be installed" \ + || $(NODEJS_PACKAGE_MANAGER) --prefix $(NODEJS_PREFIX) install \ + --save-dev \ + $(NODEJS_INSTALL_OPTS) \ + $(NODEJS_DEV_PACKAGES) + @test -z "$(NODEJS_PROD_PACKAGES)" \ + && echo "No prod packages to be installed" \ + || $(NODEJS_PACKAGE_MANAGER) --prefix $(NODEJS_PREFIX) install \ + --save-prod \ + $(NODEJS_INSTALL_OPTS) \ + $(NODEJS_PROD_PACKAGES) + @test -z "$(NODEJS_OPT_PACKAGES)" \ + && echo "No opt packages to be installed" \ + || $(NODEJS_PACKAGE_MANAGER) --prefix $(NODEJS_PREFIX) install \ + --save-optional \ + $(NODEJS_INSTALL_OPTS) \ + $(NODEJS_OPT_PACKAGES) + @test -z "$(NODEJS_PACKAGES)" \ + && echo "No packages to be installed" \ + || $(NODEJS_PACKAGE_MANAGER) --prefix $(NODEJS_PREFIX) install \ + --no-save \ + $(NODEJS_PACKAGES) + @touch $(NODEJS_TARGET) + +.PHONY: nodejs +nodejs: $(NODEJS_TARGET) + +.PHONY: nodejs-dirty +nodejs-dirty: + @rm -f $(NODEJS_TARGET) + +.PHONY: nodejs-clean +nodejs-clean: nodejs-dirty + @rm -rf $(NODEJS_PREFIX)/node_modules + +INSTALL_TARGETS+=nodejs +DIRTY_TARGETS+=nodejs-dirty +CLEAN_TARGETS+=nodejs-clean + +############################################################################## +# rollup +############################################################################## + +NODEJS_DEV_PACKAGES+=\ + rollup \ + rollup-plugin-cleanup \ + @rollup/plugin-terser + +.PHONY: rollup +rollup: $(NODEJS_TARGET) + @rollup --config $(ROLLUP_CONFIG) + +############################################################################## +# mxenv +############################################################################## + +export OS:=$(OS) + +# Determine the executable path +ifeq ("$(VENV_ENABLED)", "true") +export VIRTUAL_ENV=$(abspath $(VENV_FOLDER)) +ifeq ("$(OS)", "Windows_NT") +VENV_EXECUTABLE_FOLDER=$(VIRTUAL_ENV)/Scripts +else +VENV_EXECUTABLE_FOLDER=$(VIRTUAL_ENV)/bin +endif +export PATH:=$(VENV_EXECUTABLE_FOLDER):$(PATH) +MXENV_PYTHON=python +else +MXENV_PYTHON=$(PRIMARY_PYTHON) +endif + +# Determine the package installer +ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") +PYTHON_PACKAGE_COMMAND=uv pip +else +PYTHON_PACKAGE_COMMAND=$(MXENV_PYTHON) -m pip +endif + +MXENV_TARGET:=$(SENTINEL_FOLDER)/mxenv.sentinel +$(MXENV_TARGET): $(SENTINEL) + @$(PRIMARY_PYTHON) -c "import sys; vi = sys.version_info; sys.exit(1 if (int(vi[0]), int(vi[1])) >= tuple(map(int, '$(PYTHON_MIN_VERSION)'.split('.'))) else 0)" \ + && echo "Need Python >= $(PYTHON_MIN_VERSION)" && exit 1 || : + @[[ "$(VENV_ENABLED)" == "true" && "$(VENV_FOLDER)" == "" ]] \ + && echo "VENV_FOLDER must be configured if VENV_ENABLED is true" && exit 1 || : + @[[ "$(VENV_ENABLED)$(PYTHON_PACKAGE_INSTALLER)" == "falseuv" ]] \ + && echo "Package installer uv does not work with a global Python interpreter." && exit 1 || : +ifeq ("$(VENV_ENABLED)", "true") +ifeq ("$(VENV_CREATE)", "true") +ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvtrue") + @echo "Setup Python Virtual Environment using package 'uv' at '$(VENV_FOLDER)'" + @uv venv -p $(PRIMARY_PYTHON) --seed $(VENV_FOLDER) +else + @echo "Setup Python Virtual Environment using module 'venv' at '$(VENV_FOLDER)'" + @$(PRIMARY_PYTHON) -m venv $(VENV_FOLDER) + @$(MXENV_PYTHON) -m ensurepip -U +endif +endif +else + @echo "Using system Python interpreter" +endif +ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvfalse") + @echo "Install uv" + @$(MXENV_PYTHON) -m pip install uv +endif + @$(PYTHON_PACKAGE_COMMAND) install -U pip setuptools wheel + @echo "Install/Update MXStack Python packages" + @$(PYTHON_PACKAGE_COMMAND) install -U $(MXDEV) $(MXMAKE) + @touch $(MXENV_TARGET) + +.PHONY: mxenv +mxenv: $(MXENV_TARGET) + +.PHONY: mxenv-dirty +mxenv-dirty: + @rm -f $(MXENV_TARGET) + +.PHONY: mxenv-clean +mxenv-clean: mxenv-dirty +ifeq ("$(VENV_ENABLED)", "true") +ifeq ("$(VENV_CREATE)", "true") + @rm -rf $(VENV_FOLDER) +endif +else + @$(PYTHON_PACKAGE_COMMAND) uninstall -y $(MXDEV) + @$(PYTHON_PACKAGE_COMMAND) uninstall -y $(MXMAKE) +endif + +INSTALL_TARGETS+=mxenv +DIRTY_TARGETS+=mxenv-dirty +CLEAN_TARGETS+=mxenv-clean + +############################################################################## +# sources +############################################################################## + +SOURCES_TARGET:=$(SENTINEL_FOLDER)/sources.sentinel +$(SOURCES_TARGET): $(PROJECT_CONFIG) $(MXENV_TARGET) + @echo "Checkout project sources" + @mxdev -o -c $(PROJECT_CONFIG) + @touch $(SOURCES_TARGET) + +.PHONY: sources +sources: $(SOURCES_TARGET) + +.PHONY: sources-dirty +sources-dirty: + @rm -f $(SOURCES_TARGET) + +.PHONY: sources-purge +sources-purge: sources-dirty + @rm -rf sources + +INSTALL_TARGETS+=sources +DIRTY_TARGETS+=sources-dirty +PURGE_TARGETS+=sources-purge + +############################################################################## +# mxfiles +############################################################################## + +# case `core.sources` domain not included +SOURCES_TARGET?= + +# File generation target +MXMAKE_FILES?=$(MXMAKE_FOLDER)/files + +# set environment variables for mxmake +define set_mxfiles_env + @export MXMAKE_FILES=$(1) +endef + +# unset environment variables for mxmake +define unset_mxfiles_env + @unset MXMAKE_FILES +endef + +$(PROJECT_CONFIG): +ifneq ("$(wildcard $(PROJECT_CONFIG))","") + @touch $(PROJECT_CONFIG) +else + @echo "[settings]" > $(PROJECT_CONFIG) +endif + +LOCAL_PACKAGE_FILES:=$(wildcard pyproject.toml setup.cfg setup.py requirements.txt constraints.txt) + +FILES_TARGET:=requirements-mxdev.txt +$(FILES_TARGET): $(PROJECT_CONFIG) $(MXENV_TARGET) $(SOURCES_TARGET) $(LOCAL_PACKAGE_FILES) + @echo "Create project files" + @mkdir -p $(MXMAKE_FILES) + $(call set_mxfiles_env,$(MXMAKE_FILES)) + @mxdev -n -c $(PROJECT_CONFIG) + $(call unset_mxfiles_env) + @test -e $(MXMAKE_FILES)/pip.conf && cp $(MXMAKE_FILES)/pip.conf $(VENV_FOLDER)/pip.conf || : + @touch $(FILES_TARGET) + +.PHONY: mxfiles +mxfiles: $(FILES_TARGET) + +.PHONY: mxfiles-dirty +mxfiles-dirty: + @touch $(PROJECT_CONFIG) + +.PHONY: mxfiles-clean +mxfiles-clean: mxfiles-dirty + @rm -rf constraints-mxdev.txt requirements-mxdev.txt $(MXMAKE_FILES) + +INSTALL_TARGETS+=mxfiles +DIRTY_TARGETS+=mxfiles-dirty +CLEAN_TARGETS+=mxfiles-clean + +############################################################################## +# packages +############################################################################## + +# additional sources targets which requires package re-install on change +-include $(MXMAKE_FILES)/additional_sources_targets.mk +ADDITIONAL_SOURCES_TARGETS?= + +INSTALLED_PACKAGES=$(MXMAKE_FILES)/installed.txt + +ifeq ("$(PACKAGES_ALLOW_PRERELEASES)","true") +ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") +PACKAGES_PRERELEASES=--prerelease=allow +else +PACKAGES_PRERELEASES=--pre +endif +else +PACKAGES_PRERELEASES= +endif + +PACKAGES_TARGET:=$(INSTALLED_PACKAGES) +$(PACKAGES_TARGET): $(FILES_TARGET) $(ADDITIONAL_SOURCES_TARGETS) + @echo "Install python packages" + @$(PYTHON_PACKAGE_COMMAND) install $(PACKAGES_PRERELEASES) -r $(FILES_TARGET) + @$(PYTHON_PACKAGE_COMMAND) freeze > $(INSTALLED_PACKAGES) + @touch $(PACKAGES_TARGET) + +.PHONY: packages +packages: $(PACKAGES_TARGET) + +.PHONY: packages-dirty +packages-dirty: + @rm -f $(PACKAGES_TARGET) + +.PHONY: packages-clean +packages-clean: + @test -e $(FILES_TARGET) \ + && test -e $(MXENV_PYTHON) \ + && $(MXENV_PYTHON) -m pip uninstall -y -r $(FILES_TARGET) \ + || : + @rm -f $(PACKAGES_TARGET) + +INSTALL_TARGETS+=packages +DIRTY_TARGETS+=packages-dirty +CLEAN_TARGETS+=packages-clean + +############################################################################## +# test +############################################################################## + +TEST_TARGET:=$(SENTINEL_FOLDER)/test.sentinel +$(TEST_TARGET): $(MXENV_TARGET) + @echo "Install $(TEST_REQUIREMENTS)" + @$(PYTHON_PACKAGE_COMMAND) install $(TEST_REQUIREMENTS) + @touch $(TEST_TARGET) + +.PHONY: test +test: $(FILES_TARGET) $(SOURCES_TARGET) $(PACKAGES_TARGET) $(TEST_TARGET) $(TEST_DEPENDENCY_TARGETS) + @test -z "$(TEST_COMMAND)" && echo "No test command defined" && exit 1 || : + @echo "Run tests using $(TEST_COMMAND)" + @/usr/bin/env bash -c "$(TEST_COMMAND)" + +.PHONY: test-dirty +test-dirty: + @rm -f $(TEST_TARGET) + +.PHONY: test-clean +test-clean: test-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y $(TEST_REQUIREMENTS) || : + @rm -rf .pytest_cache + +INSTALL_TARGETS+=$(TEST_TARGET) +CLEAN_TARGETS+=test-clean +DIRTY_TARGETS+=test-dirty + +############################################################################## +# coverage +############################################################################## + +COVERAGE_TARGET:=$(SENTINEL_FOLDER)/coverage.sentinel +$(COVERAGE_TARGET): $(TEST_TARGET) + @echo "Install Coverage" + @$(PYTHON_PACKAGE_COMMAND) install -U coverage + @touch $(COVERAGE_TARGET) + +.PHONY: coverage +coverage: $(FILES_TARGET) $(SOURCES_TARGET) $(PACKAGES_TARGET) $(COVERAGE_TARGET) + @test -z "$(COVERAGE_COMMAND)" && echo "No coverage command defined" && exit 1 || : + @echo "Run coverage using $(COVERAGE_COMMAND)" + @/usr/bin/env bash -c "$(COVERAGE_COMMAND)" + +.PHONY: coverage-dirty +coverage-dirty: + @rm -f $(COVERAGE_TARGET) + +.PHONY: coverage-clean +coverage-clean: coverage-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y coverage || : + @rm -rf .coverage htmlcov + +INSTALL_TARGETS+=$(COVERAGE_TARGET) +DIRTY_TARGETS+=coverage-dirty +CLEAN_TARGETS+=coverage-clean + +############################################################################## +# zest-releaser +############################################################################## + +ZEST_RELEASER_TARGET:=$(SENTINEL_FOLDER)/zest-releaser.sentinel +$(ZEST_RELEASER_TARGET): $(MXENV_TARGET) + @echo "Install zest.releaser" + @$(PYTHON_PACKAGE_COMMAND) install zest.releaser + @touch $(ZEST_RELEASER_TARGET) + +.PHONY: zest-releaser-prerelease +zest-releaser-prerelease: $(ZEST_RELEASER_TARGET) + @echo "Run prerelease" + @prerelease $(ZEST_RELEASER_PRERELEASE_OPTIONS) + +.PHONY: zest-releaser-release +zest-releaser-release: $(ZEST_RELEASER_TARGET) + @echo "Run release" + @release $(ZEST_RELEASER_RELEASE_OPTIONS) + +.PHONY: zest-releaser-postrelease +zest-releaser-postrelease: $(ZEST_RELEASER_TARGET) + @echo "Run postrelease" + @postrelease $(ZEST_RELEASER_POSTRELEASE_OPTIONS) + +.PHONY: zest-releaser-fullrelease +zest-releaser-fullrelease: $(ZEST_RELEASER_TARGET) + @echo "Run fullrelease" + @fullrelease $(ZEST_RELEASER_FULLRELEASE_OPTIONS) + +.PHONY: zest-releaser-dirty +zest-releaser-dirty: + @rm -f $(ZEST_RELEASER_TARGET) + +.PHONY: zest-releaser-clean +zest-releaser-clean: zest-releaser-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y zest.releaser || : + +INSTALL_TARGETS+=$(ZEST_RELEASER_TARGET) +DIRTY_TARGETS+=zest-releaser-dirty +CLEAN_TARGETS+=zest-releaser-clean + +############################################################################## +# gettext +############################################################################## + +# case `system.dependencies` domain is included +SYSTEM_DEPENDENCIES+=gettext + +.PHONY: gettext-create +gettext-create: + @if [ ! -e "$(GETTEXT_LOCALES_PATH)/$(GETTEXT_DOMAIN).pot" ]; then \ + echo "Create pot file"; \ + mkdir -p "$(GETTEXT_LOCALES_PATH)"; \ + touch "$(GETTEXT_LOCALES_PATH)/$(GETTEXT_DOMAIN).pot"; \ + fi + @for lang in $(GETTEXT_LANGUAGES); do \ + if [ ! -e "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES/$(GETTEXT_DOMAIN).po" ]; then \ + mkdir -p "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES"; \ + msginit \ + -i "$(GETTEXT_LOCALES_PATH)/$(GETTEXT_DOMAIN).pot" \ + -o "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES/$(GETTEXT_DOMAIN).po" \ + -l $$lang; \ + fi \ + done + +.PHONY: gettext-update +gettext-update: + @echo "Update translations" + @for lang in $(GETTEXT_LANGUAGES); do \ + msgmerge -o \ + "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES/$(GETTEXT_DOMAIN).po" \ + "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES/$(GETTEXT_DOMAIN).po" \ + "$(GETTEXT_LOCALES_PATH)/$(GETTEXT_DOMAIN).pot"; \ + done + +.PHONY: gettext-compile +gettext-compile: + @echo "Compile message catalogs" + @for lang in $(GETTEXT_LANGUAGES); do \ + msgfmt --statistics -o \ + "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES/$(GETTEXT_DOMAIN).mo" \ + "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES/$(GETTEXT_DOMAIN).po"; \ + done + +############################################################################## +# lingua +############################################################################## + +LINGUA_TARGET:=$(SENTINEL_FOLDER)/lingua.sentinel +$(LINGUA_TARGET): $(MXENV_TARGET) + @echo "Install Lingua" + @$(PYTHON_PACKAGE_COMMAND) install chameleon lingua $(LINGUA_PLUGINS) + @touch $(LINGUA_TARGET) + +PHONY: lingua-extract +lingua-extract: $(LINGUA_TARGET) + @echo "Extract messages" + @pot-create \ + "$(LINGUA_SEARCH_PATH)" $(LINGUA_OPTIONS) \ + -o "$(GETTEXT_LOCALES_PATH)/$(GETTEXT_DOMAIN).pot" + +PHONY: lingua +lingua: gettext-create lingua-extract gettext-update gettext-compile + +.PHONY: lingua-dirty +lingua-dirty: + @rm -f $(LINGUA_TARGET) + +.PHONY: lingua-clean +lingua-clean: lingua-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y \ + chameleon lingua $(LINGUA_PLUGINS) || : + +INSTALL_TARGETS+=$(LINGUA_TARGET) +DIRTY_TARGETS+=lingua-dirty +CLEAN_TARGETS+=lingua-clean + +############################################################################## +# Custom includes +############################################################################## + +-include $(INCLUDE_MAKEFILE) + +############################################################################## +# Default targets +############################################################################## + +INSTALL_TARGET:=$(SENTINEL_FOLDER)/install.sentinel +$(INSTALL_TARGET): $(INSTALL_TARGETS) + @touch $(INSTALL_TARGET) + +.PHONY: install +install: $(INSTALL_TARGET) + @touch $(INSTALL_TARGET) + +.PHONY: run +run: $(RUN_TARGET) + +.PHONY: deploy +deploy: $(DEPLOY_TARGETS) + +.PHONY: dirty +dirty: $(DIRTY_TARGETS) + @rm -f $(INSTALL_TARGET) + +.PHONY: clean +clean: dirty $(CLEAN_TARGETS) + @rm -rf $(CLEAN_TARGETS) $(MXMAKE_FOLDER) $(CLEAN_FS) + +.PHONY: purge +purge: clean $(PURGE_TARGETS) + +.PHONY: runtime-clean +runtime-clean: + @echo "Remove runtime artifacts, like byte-code and caches." + @find . -name '*.py[c|o]' -delete + @find . -name '*~' -exec rm -f {} + + @find . -name '__pycache__' -exec rm -fr {} + + +.PHONY: check +check: $(CHECK_TARGETS) + +.PHONY: typecheck +typecheck: $(TYPECHECK_TARGETS) + +.PHONY: format +format: $(FORMAT_TARGETS) diff --git a/mx.ini b/mx.ini new file mode 100644 index 0000000..aaac142 --- /dev/null +++ b/mx.ini @@ -0,0 +1,93 @@ +[settings] +threads = 5 + +version-overrides = + pyramid==1.9.4 + repoze.zcml==1.1 + repoze.workflow==1.1 + +main-package = -e .[test] + +mxmake-templates = + run-tests + run-coverage + +mxmake-test-path = src +mxmake-source-path = src/cone/maps + +cs = https://github.com/conestack +cs_push = git@github.com:conestack +bda = https://github.com/bluedynamics +bda_push = git@github.com:bluedynamics + +[mxmake-env] +TESTRUN_MARKER = 1 + +[mxmake-run-tests] +environment = env + +[mxmake-run-coverage] +environment = env + +[odict] +url = ${settings:cs}/odict.git +pushurl = ${settings:cs_push}/odict.git +branch = master +mxmake-test-path = src +mxmake-source-path = src/odict + +[plumber] +url = ${settings:cs}/plumber.git +pushurl = ${settings:cs_push}/plumber.git +branch = master +mxmake-test-path = src +mxmake-source-path = src/plumber + +[node] +url = ${settings:cs}/node.git +pushurl = ${settings:cs_push}/node.git +branch = master +mxmake-test-path = src +mxmake-source-path = src/node + +[webresource] +url = ${settings:cs}/webresource.git +pushurl = ${settings:cs_push}/webresource.git +branch = master +mxmake-test-path = . +mxmake-source-path = webresource + +[cone.tile] +url = ${settings:cs}/cone.tile.git +pushurl = ${settings:cs_push}/cone.tile.git +branch = master +mxmake-test-path = src +mxmake-source-path = src/cone/tile + +[cone.app] +url = ${settings:cs}/cone.app.git +pushurl = ${settings:cs_push}/cone.app.git +branch = 2.0 +mxmake-test-path = src +mxmake-source-path = src/cone/app + +[yafowil] +url = ${settings:cs}/yafowil.git +pushurl = ${settings:cs_push}/yafowil.git +branch = master +mxmake-test-path = src +mxmake-source-path = src/yafowil + +[yafowil.bootstrap] +url = ${settings:cs}/yafowil.bootstrap.git +pushurl = ${settings:cs_push}/yafowil.bootstrap.git +branch = bs5 +mxmake-test-path = src +mxmake-source-path = src/yafowil/bootstrap + +[yafowil.yaml] +url = ${settings:cs}/yafowil.yaml.git +pushurl = ${settings:cs_push}/yafowil.yaml.git +branch = bs5 +mxmake-test-path = src +mxmake-source-path = src/yafowil/yaml diff --git a/package.json b/package.json index 9a32ece..b5dd0e5 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "devDependencies": { + "@rollup/plugin-terser": "^0.4.4", "karma": "^6.4.0", "karma-chrome-launcher": "^3.1.1", "karma-coverage": "^2.2.0", "karma-module-resolver-preprocessor": "^1.1.3", "karma-qunit": "^4.1.2", "qunit": "^2.19.1", - "rollup": "^2.77.2", + "rollup": "^2.79.2", "rollup-plugin-cleanup": "^3.2.1", "rollup-plugin-terser": "^7.0.2" } diff --git a/setup.py b/setup.py index 585cf98..ee5e0c7 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ from setuptools import find_packages from setuptools import setup -from setuptools.command.test import test import os @@ -18,13 +17,6 @@ def read_file(name): ]]) -class Test(test): - - def run_tests(self): - from cone.maps import tests - tests.run_tests() - - setup( name='cone.maps', version=version, @@ -47,10 +39,12 @@ def run_tests(self): zip_safe=False, install_requires=[ 'setuptools', - 'cone.app', + 'cone.app[lxml]>=1.0.3', 'yafowil.widget.location' ], - extras_require=dict(test=['zope.testrunner']), - tests_require=['zope.testrunner'], - cmdclass=dict(test=Test) + extras_require=dict( + test=[ + 'pytest', + 'zope.pytestlayer' + ]) ) diff --git a/src/cone/maps/tests.py b/src/cone/maps/tests/test_package.py similarity index 100% rename from src/cone/maps/tests.py rename to src/cone/maps/tests/test_package.py From 8814143e1a4c41b776f8ce6b7730d807764b1b29 Mon Sep 17 00:00:00 2001 From: Lena Daxenbichler Date: Mon, 18 Nov 2024 12:11:43 +0100 Subject: [PATCH 16/29] remove deprecated rollup terser plugin --- js/rollup.conf.js | 2 +- package.json | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/js/rollup.conf.js b/js/rollup.conf.js index 8896ba8..70a058e 100644 --- a/js/rollup.conf.js +++ b/js/rollup.conf.js @@ -1,5 +1,5 @@ import cleanup from 'rollup-plugin-cleanup'; -import {terser} from 'rollup-plugin-terser'; +import terser from '@rollup/plugin-terser'; const out_dir = 'src/cone/maps/browser/static/maps'; diff --git a/package.json b/package.json index b5dd0e5..52f2deb 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "karma-qunit": "^4.1.2", "qunit": "^2.19.1", "rollup": "^2.79.2", - "rollup-plugin-cleanup": "^3.2.1", - "rollup-plugin-terser": "^7.0.2" + "rollup-plugin-cleanup": "^3.2.1" } } From 5fe621bfbc706178bc5e67a08b4b4bc9ee414aa9 Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Tue, 21 Oct 2025 13:32:59 +0200 Subject: [PATCH 17/29] Refactor package layout to modern Python packaging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Convert setup.py/setup.cfg to pyproject.toml - Implement PEP 420 implicit namespaces (remove namespace declarations) - Delete namespace-only __init__.py files - Add /build to .gitignore 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .gitignore | 1 + pyproject.toml | 41 ++++++++++++++++++++++++++++++++ setup.py | 56 -------------------------------------------- src/cone/__init__.py | 1 - 4 files changed, 42 insertions(+), 57 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.py delete mode 100644 src/cone/__init__.py diff --git a/.gitignore b/.gitignore index a8ae82e..933924a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ __pycache__ /src/cone.maps.egg-info/ /node_modules/ /package-lock.json +/build diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0b7a72d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,41 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "cone.maps" +version = "0.2.dev0" +description = "Maps integration into cone using leaflet.js." +readme = "README.rst" +license = {text = "Simplified BSD"} +authors = [{name = "Cone Contributors", email = "dev@conestack.org"}] +keywords = ["node", "pyramid", "cone", "web"] +classifiers = [ + "Environment :: Web Environment", + "Programming Language :: Python", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", +] +dependencies = [ + "cone.app", + "yafowil.widget.location", +] + +[project.optional-dependencies] +test = [ + "zope.testrunner", +] + +[project.urls] +Homepage = "http://github.com/conestack/cone.maps" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools] +zip-safe = false + +[tool.setuptools.package-dir] +"" = "src" + +[tool.zest-releaser] +create-wheel = true diff --git a/setup.py b/setup.py deleted file mode 100644 index 585cf98..0000000 --- a/setup.py +++ /dev/null @@ -1,56 +0,0 @@ -from setuptools import find_packages -from setuptools import setup -from setuptools.command.test import test -import os - - -def read_file(name): - with open(os.path.join(os.path.dirname(__file__), name)) as f: - return f.read() - - -version = '0.2.dev0' -shortdesc = 'Maps integration into cone using leaflet.js.' -longdesc = '\n\n'.join([read_file(name) for name in [ - 'README.rst', - 'CHANGES.rst', - 'LICENSE.rst' -]]) - - -class Test(test): - - def run_tests(self): - from cone.maps import tests - tests.run_tests() - - -setup( - name='cone.maps', - version=version, - description=shortdesc, - long_description=longdesc, - classifiers=[ - 'Environment :: Web Environment', - 'Programming Language :: Python', - 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', - ], - keywords='node pyramid cone web', - author='Cone Contributors', - author_email='dev@conestack.org', - url='http://github.com/conestack/cone.maps', - license='Simplified BSD', - packages=find_packages('src'), - package_dir={'': 'src'}, - namespace_packages=['cone'], - include_package_data=True, - zip_safe=False, - install_requires=[ - 'setuptools', - 'cone.app', - 'yafowil.widget.location' - ], - extras_require=dict(test=['zope.testrunner']), - tests_require=['zope.testrunner'], - cmdclass=dict(test=Test) -) diff --git a/src/cone/__init__.py b/src/cone/__init__.py deleted file mode 100644 index b0d6433..0000000 --- a/src/cone/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__import__('pkg_resources').declare_namespace(__name__) \ No newline at end of file From a4e71ef0d9dbe793bf82188285d2d2b54fa91dc6 Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Tue, 21 Oct 2025 15:12:00 +0200 Subject: [PATCH 18/29] Use hatchling instead of setuptools --- pyproject.toml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0b7a72d..2f4e09b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" +requires = ["hatchling"] +build-backend = "hatchling.build" [project] name = "cone.maps" @@ -28,14 +28,5 @@ test = [ [project.urls] Homepage = "http://github.com/conestack/cone.maps" -[tool.setuptools.packages.find] -where = ["src"] - -[tool.setuptools] -zip-safe = false - -[tool.setuptools.package-dir] -"" = "src" - [tool.zest-releaser] create-wheel = true From 3fc0a3cbf16aec9da48fe7a5ae6e8ac55f4a3a3c Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Tue, 21 Oct 2025 15:49:43 +0200 Subject: [PATCH 19/29] add tool.hatch.build.targets.wheel sections --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 2f4e09b..b9370a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,5 +28,8 @@ test = [ [project.urls] Homepage = "http://github.com/conestack/cone.maps" + +[tool.hatch.build.targets.wheel] +packages = ["src/cone"] [tool.zest-releaser] create-wheel = true From 46ad0684d98d024320c3c6efb88f7ffb652eadbf Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Wed, 22 Oct 2025 14:53:49 +0200 Subject: [PATCH 20/29] Remove MANIFEST.in and update to Python 3.10-3.14 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove obsolete MANIFEST.in file (now using hatchling) - Update Python classifiers to 3.10-3.14 - Update GitHub Actions workflows to test Python 3.10-3.14 - Remove PyPy from workflows 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/python-package.yml | 2 +- MANIFEST.in | 5 ----- pyproject.toml | 6 +++++- 3 files changed, 6 insertions(+), 7 deletions(-) delete mode 100644 MANIFEST.in diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 594748e..675e1de 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.7, 3.8, 3.9] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v2 diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index e8eca2f..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -include *.rst -include *.mo -include *.po -recursive-include src * -recursive-exclude src *.pyc *.pyo diff --git a/pyproject.toml b/pyproject.toml index b9370a9..c1f9203 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,11 @@ keywords = ["node", "pyramid", "cone", "web"] classifiers = [ "Environment :: Web Environment", "Programming Language :: Python", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", ] dependencies = [ @@ -28,7 +33,6 @@ test = [ [project.urls] Homepage = "http://github.com/conestack/cone.maps" - [tool.hatch.build.targets.wheel] packages = ["src/cone"] [tool.zest-releaser] From 6d70694b4d93e604a44bca30bd113e4d935e2f27 Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Mon, 3 Nov 2025 11:34:44 +0100 Subject: [PATCH 21/29] Update package versions. --- CHANGES.rst | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index d70b4d8..a3e5528 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,8 +1,8 @@ Changes ======= -0.2 (unreleased) ----------------- +1.1.0 (unreleased) +------------------ - Add ``Path.Drag.js`` to resources. [rnix] diff --git a/pyproject.toml b/pyproject.toml index c1f9203..4bbacc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "cone.maps" -version = "0.2.dev0" +version = "1.1.0.dev0" description = "Maps integration into cone using leaflet.js." readme = "README.rst" license = {text = "Simplified BSD"} From 45ad08079ec9f3b19d1724e0db9134554247da09 Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Mon, 3 Nov 2025 15:40:22 +0100 Subject: [PATCH 22/29] Add pytest namespace packahe handling options --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 4bbacc2..0ea7381 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,5 +35,11 @@ Homepage = "http://github.com/conestack/cone.maps" [tool.hatch.build.targets.wheel] packages = ["src/cone"] + +[tool.pytest.ini_options] +consider_namespace_packages = true +addopts = ["--import-mode=importlib"] +pythonpath = "src" + [tool.zest-releaser] create-wheel = true From 2ddcdbb7fbc2779ba169889f6bdb548ec3217e23 Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Tue, 4 Nov 2025 08:06:35 +0100 Subject: [PATCH 23/29] create tests folder --- src/cone/maps/{tests.py => tests/test_maps.py} | 16 ---------------- 1 file changed, 16 deletions(-) rename src/cone/maps/{tests.py => tests/test_maps.py} (97%) diff --git a/src/cone/maps/tests.py b/src/cone/maps/tests/test_maps.py similarity index 97% rename from src/cone/maps/tests.py rename to src/cone/maps/tests/test_maps.py index a45d9d2..d03252e 100644 --- a/src/cone/maps/tests.py +++ b/src/cone/maps/tests/test_maps.py @@ -317,19 +317,3 @@ def set_resource_include(self, name, value): 'leaflet-proj4-js': True, 'cone-maps-js': True }) - - -def run_tests(): - from cone.maps import tests - from zope.testrunner.runner import Runner - - suite = unittest.TestSuite() - suite.addTest(unittest.findTestCases(tests)) - - runner = Runner(found_suites=[suite]) - runner.run() - sys.exit(int(runner.failed)) - - -if __name__ == '__main__': - run_tests() From 6b63f8d4fc59f775e462c03f102b7775685ce627 Mon Sep 17 00:00:00 2001 From: Lena Daxenbichler Date: Tue, 4 Nov 2025 09:35:03 +0100 Subject: [PATCH 24/29] pin versions in pyproject.toml --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0ea7381..34ef515 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,8 +21,8 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP :: Dynamic Content", ] dependencies = [ - "cone.app", - "yafowil.widget.location", + "cone.app>1.0.99,<2.0.0", + "yafowil.widget.location>1.99", ] [project.optional-dependencies] From c3e6bb4b29d101738416b1a4af4e571ed17441e3 Mon Sep 17 00:00:00 2001 From: Robert Niederreiter Date: Tue, 4 Nov 2025 19:17:18 +0100 Subject: [PATCH 25/29] blankline --- mx.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/mx.ini b/mx.ini index 63bb9f1..e9e9d40 100644 --- a/mx.ini +++ b/mx.ini @@ -14,7 +14,6 @@ feature_branch = refactor-package-layout # main package main-package = -e .[test] - # fixed dependency package versions version-overrides = pyramid==2.0.2 From 26aca51139f83e6d3646f0dc691a183819a2f130 Mon Sep 17 00:00:00 2001 From: Lena Daxenbichler Date: Tue, 11 Nov 2025 10:18:12 +0100 Subject: [PATCH 26/29] add hatch-fancy-pypi-readme. change package manager to pnpm. update webresource branch --- .gitignore | 1 + Makefile | 2 +- mx.ini | 2 +- package.json | 3 ++- pyproject.toml | 23 +++++++++++++++++-- .../maps/browser/static/maps/cone.maps.min.js | 2 +- 6 files changed, 27 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 101f1b8..ebe24f1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ /dist/ /node_modules/ /package-lock.json +/pnpm-lock.yaml /requirements-mxdev.txt /sources/ /venv/ diff --git a/Makefile b/Makefile index 86e6784..2eb0143 100644 --- a/Makefile +++ b/Makefile @@ -55,7 +55,7 @@ PROJECT_PATH_PYTHON?= # The package manager to use. Defaults to `npm`. Possible values # are `npm` and `pnpm` # Default: npm -NODEJS_PACKAGE_MANAGER?=npm +NODEJS_PACKAGE_MANAGER?=pnpm # Value for `--prefix` option when installing packages. # Default: . diff --git a/mx.ini b/mx.ini index e9e9d40..ce5a01f 100644 --- a/mx.ini +++ b/mx.ini @@ -137,7 +137,7 @@ branch = ${settings:feature_branch} use = ${settings:checkout_packages} url = ${settings:cs}/webresource.git pushurl = ${settings:cs_push}/webresource.git -branch = ${settings:feature_branch} +branch = master extras = test mxmake-test-path = tests mxmake-source-path = webresource diff --git a/package.json b/package.json index 52f2deb..266a651 100644 --- a/package.json +++ b/package.json @@ -9,5 +9,6 @@ "qunit": "^2.19.1", "rollup": "^2.79.2", "rollup-plugin-cleanup": "^3.2.1" - } + }, + "packageManager": "pnpm@9.3.0+sha512.ee7b93e0c2bd11409c6424f92b866f31d3ea1bef5fbe47d3c7500cdc3c9668833d2e55681ad66df5b640c61fa9dc25d546efa54d76d7f8bf54b13614ac293631" } diff --git a/pyproject.toml b/pyproject.toml index c74793f..9a7aa77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,13 @@ [build-system] -requires = ["hatchling"] +requires = ["hatchling", "hatch-fancy-pypi-readme"] build-backend = "hatchling.build" [project] name = "cone.maps" version = "1.1.0.dev0" description = "Maps integration into cone using leaflet.js." -readme = "README.rst" +dynamic = ["readme"] +requires-python = ">=3.10" license = {text = "Simplified BSD"} authors = [{name = "Cone Contributors", email = "dev@conestack.org"}] keywords = ["node", "pyramid", "cone", "web"] @@ -33,6 +34,24 @@ test = [ [project.urls] Homepage = "http://github.com/conestack/cone.maps" +[tool.hatch.version] +source = "vcs" + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/x-rst" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "README.rst" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "CHANGES.rst" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "LICENSE.rst" + [tool.hatch.build.targets.wheel] packages = ["src/cone"] diff --git a/src/cone/maps/browser/static/maps/cone.maps.min.js b/src/cone/maps/browser/static/maps/cone.maps.min.js index be22490..d16a759 100644 --- a/src/cone/maps/browser/static/maps/cone.maps.min.js +++ b/src/cone/maps/browser/static/maps/cone.maps.min.js @@ -1 +1 @@ -var cone_maps=function(e,t){"use strict";let a={tile_layer:function(e,t){e.layer_created(new L.TileLayer(t.urlTemplate,t.options),t)},geo_json:function(e,a){t.getJSON(a.dataUrl,(function(t){e.layer_created(new L.GeoJSON(t,a.options),a)}))}};class s{static initialize(e){t("div.cone-map",e).each((function(){let e=t(this),a=e.data("map-settings"),s=a.factory;new(ts.object_by_path(s))(e,a)}))}constructor(e,t){this.elem=e,this.id=e.attr("id"),this.layers=t.layers,this.default_center=t.center,this.default_zoom=t.zoom,this.default_bounds=t.bounds,this.map_options=t.options,this.control_options=t.control_options,this.markers=t.markers,this.markers_source=t.markers_source,this.marker_groups=t.groups,this.marker_groups_source=t.groups_source,this.create(),e.data("map-instance",this)}create(){this.create_map(),this.create_controls(),this.create_layers(),this.create_markers()}create_map(){this.map=new L.Map(this.id,this.map_options),this.default_bounds.length?this.map.fitBounds(this.default_bounds):this.map.setView(this.default_center,this.default_zoom)}create_controls(){this.map_layers=new L.Control.Layers([],[],this.control_options),this.map_layers.addTo(this.map)}create_layers(){for(let e of this.layers)a[e.factory](this,e)}layer_created(e,t){t.layer=e,(void 0===t.display||t.display)&&this.add_layer(e),"base"===t.category?this.map_layers.addBaseLayer(e,t.title):"overlay"===t.category&&this.map_layers.addOverlay(e,t.title)}add_layer(e){this.map.addLayer(e)}remove_layer(e){this.map.removeLayer(e)}create_markers(){for(let e of this.markers)this.create_marker(e);this.markers_source&&t.getJSON(this.markers_source,function(e){for(let t of e)this.create_marker(t)}.bind(this)),(this.markers||this.markers_source)&&this.map.on("popupopen",(function(e){let a=e.popup;ts.ajax.bind(t(a._contentNode))}))}create_marker(e){let t=new L.Marker(e.latlng,e.options).addTo(this.map);e.popup&&t.bindPopup(e.popup.content,e.popup.options)}}return t((function(){void 0!==window.ts?ts.ajax.register(s.initialize,!0):bdajax.register(s.initialize,!0)})),e.Map=s,e.layer_factories=a,Object.defineProperty(e,"__esModule",{value:!0}),e}({},jQuery); +var cone_maps=function(e,t){"use strict";let a={tile_layer:function(e,t){e.layer_created(new L.TileLayer(t.urlTemplate,t.options),t)},geo_json:function(e,a){t.getJSON(a.dataUrl,function(t){e.layer_created(new L.GeoJSON(t,a.options),a)})}};class s{static initialize(e){t("div.cone-map",e).each(function(){let e=t(this),a=e.data("map-settings"),s=a.factory;new(ts.object_by_path(s))(e,a)})}constructor(e,t){this.elem=e,this.id=e.attr("id"),this.layers=t.layers,this.default_center=t.center,this.default_zoom=t.zoom,this.default_bounds=t.bounds,this.map_options=t.options,this.control_options=t.control_options,this.markers=t.markers,this.markers_source=t.markers_source,this.marker_groups=t.groups,this.marker_groups_source=t.groups_source,this.create(),e.data("map-instance",this)}create(){this.create_map(),this.create_controls(),this.create_layers(),this.create_markers()}create_map(){this.map=new L.Map(this.id,this.map_options),this.default_bounds.length?this.map.fitBounds(this.default_bounds):this.map.setView(this.default_center,this.default_zoom)}create_controls(){this.map_layers=new L.Control.Layers([],[],this.control_options),this.map_layers.addTo(this.map)}create_layers(){for(let e of this.layers)a[e.factory](this,e)}layer_created(e,t){t.layer=e,(void 0===t.display||t.display)&&this.add_layer(e),"base"===t.category?this.map_layers.addBaseLayer(e,t.title):"overlay"===t.category&&this.map_layers.addOverlay(e,t.title)}add_layer(e){this.map.addLayer(e)}remove_layer(e){this.map.removeLayer(e)}create_markers(){for(let e of this.markers)this.create_marker(e);this.markers_source&&t.getJSON(this.markers_source,function(e){for(let t of e)this.create_marker(t)}.bind(this)),(this.markers||this.markers_source)&&this.map.on("popupopen",function(e){let a=e.popup;ts.ajax.bind(t(a._contentNode))})}create_marker(e){let t=new L.Marker(e.latlng,e.options).addTo(this.map);e.popup&&t.bindPopup(e.popup.content,e.popup.options)}}return t(function(){void 0!==window.ts?ts.ajax.register(s.initialize,!0):bdajax.register(s.initialize,!0)}),e.Map=s,e.layer_factories=a,Object.defineProperty(e,"__esModule",{value:!0}),e}({},jQuery); From 65a9284ae44bce9197a81deed9eb05a620b8125e Mon Sep 17 00:00:00 2001 From: Lena Daxenbichler Date: Thu, 13 Nov 2025 10:02:50 +0100 Subject: [PATCH 27/29] remove obsolete lines in pyproject.toml --- pyproject.toml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9a7aa77..776d5a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,12 +34,6 @@ test = [ [project.urls] Homepage = "http://github.com/conestack/cone.maps" -[tool.hatch.version] -source = "vcs" - -[tool.hatch.metadata] -allow-direct-references = true - [tool.hatch.metadata.hooks.fancy-pypi-readme] content-type = "text/x-rst" From d36bd2d8514729475e471e8d408a9fb6687e864a Mon Sep 17 00:00:00 2001 From: Lena Daxenbichler Date: Tue, 25 Nov 2025 12:15:52 +0100 Subject: [PATCH 28/29] change venv directory to 'venv'. remove unnecessary files from build --- .gitignore | 1 - Makefile | 2 +- pyproject.toml | 11 +++++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index ebe24f1..a93475f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ *.pyc /.coverage /.mxmake/ -/.venv/ /build/ /constraints-mxdev.txt /dist/ diff --git a/Makefile b/Makefile index 2eb0143..0906617 100644 --- a/Makefile +++ b/Makefile @@ -134,7 +134,7 @@ VENV_CREATE?=true # `VENV_CREATE` is false it is expected to point to an existing virtual # environment. If `VENV_ENABLED` is `false` it is ignored. # Default: .venv -VENV_FOLDER?=.venv +VENV_FOLDER?=venv # mxdev to install in virtual environment. # Default: mxdev diff --git a/pyproject.toml b/pyproject.toml index 776d5a9..09d76a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,17 @@ path = "CHANGES.rst" [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] path = "LICENSE.rst" +[tool.hatch.build.targets.sdist] +exclude = [ + "/.github/", + "/js/", + "/Makefile", + "/mx.ini", + "/package.json", + "/pnpm-lock.yaml", + "/scripts/", +] + [tool.hatch.build.targets.wheel] packages = ["src/cone"] From 1040de5b5849ab00129ca45b206701203aea1e3e Mon Sep 17 00:00:00 2001 From: Lena Daxenbichler Date: Thu, 27 Nov 2025 09:04:39 +0100 Subject: [PATCH 29/29] define fancy-pypi-readme fragments as regular array --- pyproject.toml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 09d76a8..6855e51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,15 +36,13 @@ Homepage = "http://github.com/conestack/cone.maps" [tool.hatch.metadata.hooks.fancy-pypi-readme] content-type = "text/x-rst" - -[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] -path = "README.rst" - -[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] -path = "CHANGES.rst" - -[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] -path = "LICENSE.rst" +fragments = [ + {path = "README.rst"}, + {text = "\n\n"}, + {path = "CHANGES.rst"}, + {text = "\n\n"}, + {path = "LICENSE.rst"}, +] [tool.hatch.build.targets.sdist] exclude = [