FR10020: Add "within <n> km/miles/degree" filter (#382)

This commit is contained in:
Serge Noiraud 2017-05-13 23:01:55 +02:00 committed by Nick Hall
parent 022da0cb82
commit 543661d62e
7 changed files with 237 additions and 3 deletions

View File

@ -3,6 +3,7 @@
#
# Copyright (C) 2002-2007 Donald N. Allingham
# Copyright (C) 2007-2008 Brian G. Matherly
# 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
@ -46,6 +47,7 @@ from ._matchessourceconfidence import MatchesSourceConfidence
from ._changedsince import ChangedSince
from ._hastag import HasTag
from ._hastitle import HasTitle
from ._withinarea import WithinArea
editor_rule_list = [
AllPlaces,
@ -68,5 +70,6 @@ editor_rule_list = [
ChangedSince,
HasTag,
HasTitle,
WithinArea,
IsEnclosedBy
]

View 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

View File

@ -2,6 +2,7 @@
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2016 Tom Samstag
# 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
@ -33,7 +34,9 @@ from ..place import (
AllPlaces, HasCitation, HasGallery, HasIdOf, RegExpIdOf, HasNote,
HasNoteRegexp, HasReferenceCountOf, HasSourceCount, HasSourceOf,
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"))
EXAMPLE = os.path.join(TEST_DIR, "example.gramps")
@ -217,6 +220,15 @@ class BaseTest(unittest.TestCase):
b'V6ALQCZZFN996CO4D', b'OC6LQCXMKP6NUVYQD8', b'CUUKQC6BY5LAZXLXC6',
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):
"""
Test IsEnclosedBy rule with inclusive option.

View File

@ -581,6 +581,9 @@ class EditRule(ManagedWindow):
long_days = displayer.long_days
days_of_week = long_days[2:] + long_days[1:2]
t = MyList(list(map(str, range(7))), days_of_week)
elif v == _('Units:'):
t = MyList([0, 1, 2],
[_('kilometers'), _('miles'), _('degrees')])
else:
t = MyEntry()
t.set_hexpand(True)

View File

@ -4,6 +4,7 @@
# Copyright (C) 2002-2006 Donald N. Allingham
# Copyright (C) 2008 Gary Burton
# Copyright (C) 2010,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
@ -46,7 +47,7 @@ from .. import build_filter_model
from . import SidebarFilter
from gramps.gen.filters import GenericFilterFactory, rules
from gramps.gen.filters.rules.place import (RegExpIdOf, HasData, IsEnclosedBy,
HasTag, HasNoteRegexp,
HasTag, HasNoteRegexp, WithinArea,
MatchesFilter)
GenericPlaceFilter = GenericFilterFactory('Place')
@ -65,6 +66,7 @@ class PlaceSidebarFilter(SidebarFilter):
self.filter_place = Place()
self.filter_place.set_type((PlaceType.CUSTOM, ''))
self.ptype = Gtk.ComboBox(has_entry=True)
self.dbstate = dbstate
if dbstate.is_open():
self.custom_types = dbstate.db.get_place_types()
else:
@ -80,6 +82,7 @@ class PlaceSidebarFilter(SidebarFilter):
self.filter_code = widgets.BasicEntry()
self.filter_enclosed = widgets.PlaceEntry(dbstate, uistate, [])
self.filter_note = widgets.BasicEntry()
self.filter_within = widgets.PlaceWithin(dbstate, uistate, [])
self.filter_regex = Gtk.CheckButton(label=_('Use regular expressions'))
self.tag = Gtk.ComboBox()
@ -106,6 +109,7 @@ class PlaceSidebarFilter(SidebarFilter):
self.add_entry(_('Type'), self.ptype)
self.add_text_entry(_('Code'), self.filter_code)
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_entry(_('Tag'), self.tag)
self.add_filter_entry(_('Custom filter'), self.generic)
@ -117,6 +121,7 @@ class PlaceSidebarFilter(SidebarFilter):
self.filter_code.set_text('')
self.filter_enclosed.set_text('')
self.filter_note.set_text('')
self.filter_within.set_value(0, 0)
self.ptype.get_child().set_text('')
self.tag.set_active(0)
self.generic.set_active(0)
@ -128,12 +133,13 @@ class PlaceSidebarFilter(SidebarFilter):
code = str(self.filter_code.get_text()).strip()
enclosed = str(self.filter_enclosed.get_text()).strip()
note = str(self.filter_note.get_text()).strip()
within = self.filter_within.get_value()
regex = self.filter_regex.get_active()
tag = self.tag.get_active() > 0
gen = self.generic.get_active() > 0
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:
generic_filter = None
else:
@ -153,6 +159,15 @@ class PlaceSidebarFilter(SidebarFilter):
rule = HasNoteRegexp([note], use_regex=regex)
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
if tag:
model = self.tag.get_model()

View File

@ -3,6 +3,7 @@
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2008 Zsolt Foldvari
# 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
@ -44,3 +45,4 @@ from .validatedcomboentry import *
from .validatedmaskedentry import *
from .valueaction import *
from .valuetoolitem import *
from .placewithin import *

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