FR10020: Add "within <n> km/miles/degree" filter (#382)
This commit is contained in:
parent
022da0cb82
commit
543661d62e
@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
# Copyright (C) 2002-2007 Donald N. Allingham
|
# Copyright (C) 2002-2007 Donald N. Allingham
|
||||||
# Copyright (C) 2007-2008 Brian G. Matherly
|
# Copyright (C) 2007-2008 Brian G. Matherly
|
||||||
|
# Copyright (C) 2017- Serge Noiraud
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -46,6 +47,7 @@ from ._matchessourceconfidence import MatchesSourceConfidence
|
|||||||
from ._changedsince import ChangedSince
|
from ._changedsince import ChangedSince
|
||||||
from ._hastag import HasTag
|
from ._hastag import HasTag
|
||||||
from ._hastitle import HasTitle
|
from ._hastitle import HasTitle
|
||||||
|
from ._withinarea import WithinArea
|
||||||
|
|
||||||
editor_rule_list = [
|
editor_rule_list = [
|
||||||
AllPlaces,
|
AllPlaces,
|
||||||
@ -68,5 +70,6 @@ editor_rule_list = [
|
|||||||
ChangedSince,
|
ChangedSince,
|
||||||
HasTag,
|
HasTag,
|
||||||
HasTitle,
|
HasTitle,
|
||||||
|
WithinArea,
|
||||||
IsEnclosedBy
|
IsEnclosedBy
|
||||||
]
|
]
|
||||||
|
96
gramps/gen/filters/rules/place/_withinarea.py
Normal file
96
gramps/gen/filters/rules/place/_withinarea.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
#
|
||||||
|
# Gramps - a GTK+/GNOME based genealogy program
|
||||||
|
#
|
||||||
|
# Copyright (C) 2002-2006 Donald N. Allingham
|
||||||
|
# Copyright (C) 2015 Nick Hall
|
||||||
|
# Copyright (C) 2017- Serge Noiraud
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
#
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Standard Python modules
|
||||||
|
#
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
from math import pi, cos, hypot
|
||||||
|
from ....const import GRAMPS_LOCALE as glocale
|
||||||
|
_ = glocale.translation.sgettext
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Gramps modules
|
||||||
|
#
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
from .. import Rule
|
||||||
|
from ....utils.location import located_in
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# WithinArea
|
||||||
|
#
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
class WithinArea(Rule):
|
||||||
|
"""
|
||||||
|
Rule that checks for a place within an area
|
||||||
|
"""
|
||||||
|
|
||||||
|
labels = [_('ID:'), _('Value:'), _('Units:')]
|
||||||
|
name = _('Places within an area')
|
||||||
|
description = _('Matches places within a given distance of another place')
|
||||||
|
category = _('Position filters')
|
||||||
|
|
||||||
|
def prepare(self, db, user):
|
||||||
|
ref_place = db.get_place_from_gramps_id(self.list[0])
|
||||||
|
self.handle = None
|
||||||
|
self.radius = None
|
||||||
|
self.latitude = None
|
||||||
|
self.longitude = None
|
||||||
|
if ref_place:
|
||||||
|
self.handle = ref_place.handle
|
||||||
|
self.latitude = ref_place.get_latitude()
|
||||||
|
if self.latitude == "":
|
||||||
|
self.latitude = None
|
||||||
|
return
|
||||||
|
self.longitude = ref_place.get_longitude()
|
||||||
|
value = self.list[1]
|
||||||
|
unit = self.list[2]
|
||||||
|
# earth perimeter in kilometers for latitude
|
||||||
|
# 2 * pi * (6371 * cos(latitude/180*pi))
|
||||||
|
# so 1 degree correspond to the result above / 360
|
||||||
|
earth_perimeter = 2*pi*(6371*cos(float(self.latitude)/180*pi))
|
||||||
|
if unit == 0: # kilometers
|
||||||
|
self.radius = float(value / (earth_perimeter/360))
|
||||||
|
elif unit == 1: # miles
|
||||||
|
self.radius = float((value / (earth_perimeter/360))/0.62138)
|
||||||
|
else: # degrees
|
||||||
|
self.radius = float(value)
|
||||||
|
self.radius = self.radius/2
|
||||||
|
|
||||||
|
def apply(self, db, place):
|
||||||
|
if self.handle is None:
|
||||||
|
return False
|
||||||
|
if self.latitude is None:
|
||||||
|
return False
|
||||||
|
if self.longitude is None:
|
||||||
|
return False
|
||||||
|
if place:
|
||||||
|
lat = place.get_latitude()
|
||||||
|
lon = place.get_longitude()
|
||||||
|
if lat and lon:
|
||||||
|
if (hypot(float(self.latitude)-float(lat),
|
||||||
|
float(self.longitude)-float(lon)) <= self.radius) == True:
|
||||||
|
return True
|
||||||
|
return False
|
@ -2,6 +2,7 @@
|
|||||||
# Gramps - a GTK+/GNOME based genealogy program
|
# Gramps - a GTK+/GNOME based genealogy program
|
||||||
#
|
#
|
||||||
# Copyright (C) 2016 Tom Samstag
|
# Copyright (C) 2016 Tom Samstag
|
||||||
|
# Copyright (C) 2017 Serge Noiraud
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -33,7 +34,9 @@ from ..place import (
|
|||||||
AllPlaces, HasCitation, HasGallery, HasIdOf, RegExpIdOf, HasNote,
|
AllPlaces, HasCitation, HasGallery, HasIdOf, RegExpIdOf, HasNote,
|
||||||
HasNoteRegexp, HasReferenceCountOf, HasSourceCount, HasSourceOf,
|
HasNoteRegexp, HasReferenceCountOf, HasSourceCount, HasSourceOf,
|
||||||
PlacePrivate, MatchesSourceConfidence, HasData, HasNoLatOrLon,
|
PlacePrivate, MatchesSourceConfidence, HasData, HasNoLatOrLon,
|
||||||
InLatLonNeighborhood, ChangedSince, HasTag, HasTitle, IsEnclosedBy)
|
InLatLonNeighborhood, ChangedSince, HasTag, HasTitle, IsEnclosedBy,
|
||||||
|
WithinArea
|
||||||
|
)
|
||||||
|
|
||||||
TEST_DIR = os.path.abspath(os.path.join(DATA_DIR, "tests"))
|
TEST_DIR = os.path.abspath(os.path.join(DATA_DIR, "tests"))
|
||||||
EXAMPLE = os.path.join(TEST_DIR, "example.gramps")
|
EXAMPLE = os.path.join(TEST_DIR, "example.gramps")
|
||||||
@ -217,6 +220,15 @@ class BaseTest(unittest.TestCase):
|
|||||||
b'V6ALQCZZFN996CO4D', b'OC6LQCXMKP6NUVYQD8', b'CUUKQC6BY5LAZXLXC6',
|
b'V6ALQCZZFN996CO4D', b'OC6LQCXMKP6NUVYQD8', b'CUUKQC6BY5LAZXLXC6',
|
||||||
b'PTFKQCKPHO2VC5SYKS', b'PHUJQCJ9R4XQO5Y0WS']))
|
b'PTFKQCKPHO2VC5SYKS', b'PHUJQCJ9R4XQO5Y0WS']))
|
||||||
|
|
||||||
|
def test_withinarea(self):
|
||||||
|
"""
|
||||||
|
Test within area rule.
|
||||||
|
"""
|
||||||
|
rule = WithinArea(['P1339', 100, 0])
|
||||||
|
self.assertEqual(self.filter_with_rule(rule), set([
|
||||||
|
b'KJUJQCY580EB77WIVO', b'TLVJQC4FD2CD9OYAXU', b'TE4KQCL9FDYA4PB6VW',
|
||||||
|
b'W9GLQCSRJIQ9N2TGDF']))
|
||||||
|
|
||||||
def test_isenclosedby_inclusive(self):
|
def test_isenclosedby_inclusive(self):
|
||||||
"""
|
"""
|
||||||
Test IsEnclosedBy rule with inclusive option.
|
Test IsEnclosedBy rule with inclusive option.
|
||||||
|
@ -581,6 +581,9 @@ class EditRule(ManagedWindow):
|
|||||||
long_days = displayer.long_days
|
long_days = displayer.long_days
|
||||||
days_of_week = long_days[2:] + long_days[1:2]
|
days_of_week = long_days[2:] + long_days[1:2]
|
||||||
t = MyList(list(map(str, range(7))), days_of_week)
|
t = MyList(list(map(str, range(7))), days_of_week)
|
||||||
|
elif v == _('Units:'):
|
||||||
|
t = MyList([0, 1, 2],
|
||||||
|
[_('kilometers'), _('miles'), _('degrees')])
|
||||||
else:
|
else:
|
||||||
t = MyEntry()
|
t = MyEntry()
|
||||||
t.set_hexpand(True)
|
t.set_hexpand(True)
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
# Copyright (C) 2002-2006 Donald N. Allingham
|
# Copyright (C) 2002-2006 Donald N. Allingham
|
||||||
# Copyright (C) 2008 Gary Burton
|
# Copyright (C) 2008 Gary Burton
|
||||||
# Copyright (C) 2010,2015 Nick Hall
|
# Copyright (C) 2010,2015 Nick Hall
|
||||||
|
# Copyright (C) 2017- Serge Noiraud
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -46,7 +47,7 @@ from .. import build_filter_model
|
|||||||
from . import SidebarFilter
|
from . import SidebarFilter
|
||||||
from gramps.gen.filters import GenericFilterFactory, rules
|
from gramps.gen.filters import GenericFilterFactory, rules
|
||||||
from gramps.gen.filters.rules.place import (RegExpIdOf, HasData, IsEnclosedBy,
|
from gramps.gen.filters.rules.place import (RegExpIdOf, HasData, IsEnclosedBy,
|
||||||
HasTag, HasNoteRegexp,
|
HasTag, HasNoteRegexp, WithinArea,
|
||||||
MatchesFilter)
|
MatchesFilter)
|
||||||
|
|
||||||
GenericPlaceFilter = GenericFilterFactory('Place')
|
GenericPlaceFilter = GenericFilterFactory('Place')
|
||||||
@ -65,6 +66,7 @@ class PlaceSidebarFilter(SidebarFilter):
|
|||||||
self.filter_place = Place()
|
self.filter_place = Place()
|
||||||
self.filter_place.set_type((PlaceType.CUSTOM, ''))
|
self.filter_place.set_type((PlaceType.CUSTOM, ''))
|
||||||
self.ptype = Gtk.ComboBox(has_entry=True)
|
self.ptype = Gtk.ComboBox(has_entry=True)
|
||||||
|
self.dbstate = dbstate
|
||||||
if dbstate.is_open():
|
if dbstate.is_open():
|
||||||
self.custom_types = dbstate.db.get_place_types()
|
self.custom_types = dbstate.db.get_place_types()
|
||||||
else:
|
else:
|
||||||
@ -80,6 +82,7 @@ class PlaceSidebarFilter(SidebarFilter):
|
|||||||
self.filter_code = widgets.BasicEntry()
|
self.filter_code = widgets.BasicEntry()
|
||||||
self.filter_enclosed = widgets.PlaceEntry(dbstate, uistate, [])
|
self.filter_enclosed = widgets.PlaceEntry(dbstate, uistate, [])
|
||||||
self.filter_note = widgets.BasicEntry()
|
self.filter_note = widgets.BasicEntry()
|
||||||
|
self.filter_within = widgets.PlaceWithin(dbstate, uistate, [])
|
||||||
|
|
||||||
self.filter_regex = Gtk.CheckButton(label=_('Use regular expressions'))
|
self.filter_regex = Gtk.CheckButton(label=_('Use regular expressions'))
|
||||||
self.tag = Gtk.ComboBox()
|
self.tag = Gtk.ComboBox()
|
||||||
@ -106,6 +109,7 @@ class PlaceSidebarFilter(SidebarFilter):
|
|||||||
self.add_entry(_('Type'), self.ptype)
|
self.add_entry(_('Type'), self.ptype)
|
||||||
self.add_text_entry(_('Code'), self.filter_code)
|
self.add_text_entry(_('Code'), self.filter_code)
|
||||||
self.add_text_entry(_('Enclosed By'), self.filter_enclosed)
|
self.add_text_entry(_('Enclosed By'), self.filter_enclosed)
|
||||||
|
self.add_text_entry(_('Within'), self.filter_within)
|
||||||
self.add_text_entry(_('Note'), self.filter_note)
|
self.add_text_entry(_('Note'), self.filter_note)
|
||||||
self.add_entry(_('Tag'), self.tag)
|
self.add_entry(_('Tag'), self.tag)
|
||||||
self.add_filter_entry(_('Custom filter'), self.generic)
|
self.add_filter_entry(_('Custom filter'), self.generic)
|
||||||
@ -117,6 +121,7 @@ class PlaceSidebarFilter(SidebarFilter):
|
|||||||
self.filter_code.set_text('')
|
self.filter_code.set_text('')
|
||||||
self.filter_enclosed.set_text('')
|
self.filter_enclosed.set_text('')
|
||||||
self.filter_note.set_text('')
|
self.filter_note.set_text('')
|
||||||
|
self.filter_within.set_value(0, 0)
|
||||||
self.ptype.get_child().set_text('')
|
self.ptype.get_child().set_text('')
|
||||||
self.tag.set_active(0)
|
self.tag.set_active(0)
|
||||||
self.generic.set_active(0)
|
self.generic.set_active(0)
|
||||||
@ -128,12 +133,13 @@ class PlaceSidebarFilter(SidebarFilter):
|
|||||||
code = str(self.filter_code.get_text()).strip()
|
code = str(self.filter_code.get_text()).strip()
|
||||||
enclosed = str(self.filter_enclosed.get_text()).strip()
|
enclosed = str(self.filter_enclosed.get_text()).strip()
|
||||||
note = str(self.filter_note.get_text()).strip()
|
note = str(self.filter_note.get_text()).strip()
|
||||||
|
within = self.filter_within.get_value()
|
||||||
regex = self.filter_regex.get_active()
|
regex = self.filter_regex.get_active()
|
||||||
tag = self.tag.get_active() > 0
|
tag = self.tag.get_active() > 0
|
||||||
gen = self.generic.get_active() > 0
|
gen = self.generic.get_active() > 0
|
||||||
|
|
||||||
empty = not (gid or name or ptype or code or enclosed or note or regex
|
empty = not (gid or name or ptype or code or enclosed or note or regex
|
||||||
or tag or gen)
|
or within or tag or gen)
|
||||||
if empty:
|
if empty:
|
||||||
generic_filter = None
|
generic_filter = None
|
||||||
else:
|
else:
|
||||||
@ -153,6 +159,15 @@ class PlaceSidebarFilter(SidebarFilter):
|
|||||||
rule = HasNoteRegexp([note], use_regex=regex)
|
rule = HasNoteRegexp([note], use_regex=regex)
|
||||||
generic_filter.add_rule(rule)
|
generic_filter.add_rule(rule)
|
||||||
|
|
||||||
|
if within and within[0] > 0 and self.dbstate.is_open():
|
||||||
|
rule = WithinArea([None, within[0], within[1]])
|
||||||
|
active_ref = self.uistate.get_active('Place')
|
||||||
|
if active_ref:
|
||||||
|
place = self.dbstate.db.get_place_from_handle(active_ref)
|
||||||
|
gid = place.get_gramps_id()
|
||||||
|
rule = WithinArea([gid, within[0], within[1]])
|
||||||
|
generic_filter.add_rule(rule)
|
||||||
|
|
||||||
# check the Tag
|
# check the Tag
|
||||||
if tag:
|
if tag:
|
||||||
model = self.tag.get_model()
|
model = self.tag.get_model()
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
# Gramps - a GTK+/GNOME based genealogy program
|
# Gramps - a GTK+/GNOME based genealogy program
|
||||||
#
|
#
|
||||||
# Copyright (C) 2008 Zsolt Foldvari
|
# Copyright (C) 2008 Zsolt Foldvari
|
||||||
|
# Copyright (C) 2017 Serge Noiraud
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# This program is free software; you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
@ -44,3 +45,4 @@ from .validatedcomboentry import *
|
|||||||
from .validatedmaskedentry import *
|
from .validatedmaskedentry import *
|
||||||
from .valueaction import *
|
from .valueaction import *
|
||||||
from .valuetoolitem import *
|
from .valuetoolitem import *
|
||||||
|
from .placewithin import *
|
||||||
|
103
gramps/gui/widgets/placewithin.py
Normal file
103
gramps/gui/widgets/placewithin.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
#
|
||||||
|
# Gramps - a GTK+/GNOME based genealogy program
|
||||||
|
#
|
||||||
|
# Copyright (C) 2015 Nick Hall
|
||||||
|
# Copyright (C) 2017- Serge Noiraud
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
#
|
||||||
|
|
||||||
|
__all__ = ["PlaceWithin"]
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Standard python modules
|
||||||
|
#
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
import logging
|
||||||
|
_LOG = logging.getLogger(".widgets.placewithin")
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# GTK/Gnome modules
|
||||||
|
#
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
from gi.repository import Gtk
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Gramps modules
|
||||||
|
#
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
from ..selectors import SelectorFactory
|
||||||
|
from gramps.gen.display.place import displayer as _pd
|
||||||
|
from gramps.gen.const import GRAMPS_LOCALE as glocale
|
||||||
|
_ = glocale.translation.gettext
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# PlaceWithin class
|
||||||
|
#
|
||||||
|
#-------------------------------------------------------------------------
|
||||||
|
class PlaceWithin(Gtk.Box):
|
||||||
|
|
||||||
|
def __init__(self, dbstate, uistate, track):
|
||||||
|
Gtk.Box.__init__(self)
|
||||||
|
self.dbstate = dbstate
|
||||||
|
self.uistate = uistate
|
||||||
|
self.track = track
|
||||||
|
self.last = ""
|
||||||
|
# initial tooltip when no place already selected.
|
||||||
|
self.tooltip = _('Matches places within a given distance'
|
||||||
|
' of the active place. You have no active place.')
|
||||||
|
self.set_tooltip_text(self.tooltip)
|
||||||
|
self.entry = Gtk.Entry()
|
||||||
|
self.entry.set_max_length(3)
|
||||||
|
self.entry.set_width_chars(5)
|
||||||
|
self.entry.connect('changed', self.entry_change)
|
||||||
|
self.pack_start(self.entry, True, True, 0)
|
||||||
|
self.unit = Gtk.ComboBoxText()
|
||||||
|
list(map(self.unit.append_text,
|
||||||
|
[ _('kilometers'), _('miles'), _('degrees') ]))
|
||||||
|
self.unit.set_active(0)
|
||||||
|
self.pack_start(self.unit, False, True, 0)
|
||||||
|
self.show_all()
|
||||||
|
|
||||||
|
def get_value(self):
|
||||||
|
value = self.entry.get_text()
|
||||||
|
if value == "":
|
||||||
|
value = "0"
|
||||||
|
return int(value), self.unit.get_active()
|
||||||
|
|
||||||
|
def set_value(self, value, unit):
|
||||||
|
self.entry.set_text(str(value))
|
||||||
|
self.unit.set_active(int(unit))
|
||||||
|
|
||||||
|
def entry_change(self, entry):
|
||||||
|
value = entry.get_text()
|
||||||
|
if value.isnumeric() or value == "":
|
||||||
|
self.last = value # This entry is numeric and valid.
|
||||||
|
else:
|
||||||
|
entry.set_text(self.last) # reset to the last valid entry
|
||||||
|
_db = self.dbstate.db
|
||||||
|
active_reference = self.uistate.get_active('Place')
|
||||||
|
place_name = None
|
||||||
|
if active_reference:
|
||||||
|
place = _db.get_place_from_handle(active_reference)
|
||||||
|
place_name = _pd.display(self.dbstate.db, place)
|
||||||
|
if place_name is None:
|
||||||
|
self.set_tooltip_text(self.tooltip)
|
||||||
|
else:
|
||||||
|
self.set_tooltip_text(place_name)
|
Loading…
Reference in New Issue
Block a user