diff --git a/gramps/gen/filters/rules/place/__init__.py b/gramps/gen/filters/rules/place/__init__.py index 601347b7b..793cf3ad8 100644 --- a/gramps/gen/filters/rules/place/__init__.py +++ b/gramps/gen/filters/rules/place/__init__.py @@ -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 ] diff --git a/gramps/gen/filters/rules/place/_withinarea.py b/gramps/gen/filters/rules/place/_withinarea.py new file mode 100644 index 000000000..6019415e9 --- /dev/null +++ b/gramps/gen/filters/rules/place/_withinarea.py @@ -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 diff --git a/gramps/gen/filters/rules/test/place_rules_test.py b/gramps/gen/filters/rules/test/place_rules_test.py index 8d8c44afe..a4390e78f 100644 --- a/gramps/gen/filters/rules/test/place_rules_test.py +++ b/gramps/gen/filters/rules/test/place_rules_test.py @@ -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. diff --git a/gramps/gui/editors/filtereditor.py b/gramps/gui/editors/filtereditor.py index f743e7209..4d19a5944 100644 --- a/gramps/gui/editors/filtereditor.py +++ b/gramps/gui/editors/filtereditor.py @@ -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) diff --git a/gramps/gui/filters/sidebar/_placesidebarfilter.py b/gramps/gui/filters/sidebar/_placesidebarfilter.py index 501d67cb1..628b9f590 100644 --- a/gramps/gui/filters/sidebar/_placesidebarfilter.py +++ b/gramps/gui/filters/sidebar/_placesidebarfilter.py @@ -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() diff --git a/gramps/gui/widgets/__init__.py b/gramps/gui/widgets/__init__.py index 8aec265e2..25177d915 100644 --- a/gramps/gui/widgets/__init__.py +++ b/gramps/gui/widgets/__init__.py @@ -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 * diff --git a/gramps/gui/widgets/placewithin.py b/gramps/gui/widgets/placewithin.py new file mode 100644 index 000000000..2fb76e1eb --- /dev/null +++ b/gramps/gui/widgets/placewithin.py @@ -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)