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

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 # 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.

View File

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

View File

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

View File

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

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)