]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/tools/special_phrases.py
Changed phrase_settings.py to phrase-settings.json and added migration function for...
[nominatim.git] / nominatim / tools / special_phrases.py
1 """
2     Functions to import special phrases into the database.
3 """
4 import logging
5 import os
6 import re
7 import subprocess
8 import sys
9 import json
10 from os.path import isfile
11 from psycopg2.sql import Identifier, Literal, SQL
12 from nominatim.tools.exec_utils import get_url
13
14 LOG = logging.getLogger()
15
16 def import_from_wiki(args, db_connection, languages=None):
17     # pylint: disable-msg=too-many-locals
18     """
19         Iterate through all specified languages and
20         extract corresponding special phrases from the wiki.
21     """
22     black_list, white_list = _load_white_and_black_lists(args)
23
24     #Compile the match regex to increase performance for the following loop.
25     occurence_pattern = re.compile(
26         r'\| ([^\|]+) \|\| ([^\|]+) \|\| ([^\|]+) \|\| ([^\|]+) \|\| ([\-YN])'
27     )
28     sanity_check_pattern = re.compile(r'^\w+$')
29
30     languages = _get_languages(args.config) if not languages else languages
31
32     #array for pairs of class/type
33     pairs = dict()
34     for lang in languages:
35         LOG.warning('Import phrases for lang: %s', lang)
36         wiki_page_xml_content = _get_wiki_content(lang)
37         #One match will be of format [label, class, type, operator, plural]
38         matches = occurence_pattern.findall(wiki_page_xml_content)
39
40         for match in matches:
41             phrase_label = match[0].strip()
42             phrase_class = match[1].strip()
43             phrase_type = match[2].strip()
44             phrase_operator = match[3].strip()
45             #hack around a bug where building=yes was imported withq quotes into the wiki
46             phrase_type = re.sub(r'\"|"', '', phrase_type)
47
48             #sanity check, in case somebody added garbage in the wiki
49             _check_sanity(lang, phrase_class, phrase_type, sanity_check_pattern)
50
51             #blacklisting: disallow certain class/type combinations
52             if phrase_class in black_list.keys() and phrase_type in black_list[phrase_class]:
53                 continue
54             #whitelisting: if class is in whitelist, allow only tags in the list
55             if phrase_class in white_list.keys() and phrase_type not in white_list[phrase_class]:
56                 continue
57
58             #add class/type to the pairs dict
59             pairs[f'{phrase_class}|{phrase_type}'] = (phrase_class, phrase_type)
60
61             _process_amenity(
62                 db_connection, phrase_label, phrase_class, phrase_type, phrase_operator
63             )
64
65     _create_place_classtype_table_and_indexes(db_connection, args.config, pairs)
66     db_connection.commit()
67     LOG.warning('Import done.')
68
69 def _load_white_and_black_lists(args):
70     """
71         Load white and black lists from phrases-settings.json.
72     """
73     config = args.config
74     settings_path = str(config.config_dir)+'/phrase-settings.json'
75
76     if config.PHRASE_CONFIG:
77         settings_path = _convert_php_settings_if_needed(args, config.PHRASE_CONFIG)
78
79     with open(settings_path, "r") as json_settings:
80         settings = json.load(json_settings)
81     return settings['blackList'], settings['whiteList']
82
83 def _get_languages(config):
84     """
85         Get list of all languages from env config file
86         or default if there is no languages configured.
87         The system will extract special phrases only from all specified languages.
88     """
89     default_languages = [
90         'af', 'ar', 'br', 'ca', 'cs', 'de', 'en', 'es',
91         'et', 'eu', 'fa', 'fi', 'fr', 'gl', 'hr', 'hu',
92         'ia', 'is', 'it', 'ja', 'mk', 'nl', 'no', 'pl',
93         'ps', 'pt', 'ru', 'sk', 'sl', 'sv', 'uk', 'vi']
94     return config.LANGUAGES or default_languages
95
96
97 def _get_wiki_content(lang):
98     """
99         Request and return the wiki page's content
100         corresponding to special phrases for a given lang.
101         Requested URL Example :
102             https://wiki.openstreetmap.org/wiki/Special:Export/Nominatim/Special_Phrases/EN
103     """
104     url = 'https://wiki.openstreetmap.org/wiki/Special:Export/Nominatim/Special_Phrases/' + lang.upper() # pylint: disable=line-too-long
105     return get_url(url)
106
107
108 def _check_sanity(lang, phrase_class, phrase_type, pattern):
109     """
110         Check sanity of given inputs in case somebody added garbage in the wiki.
111         If a bad class/type is detected the system will exit with an error.
112     """
113     try:
114         if len(pattern.findall(phrase_class)) < 1 or len(pattern.findall(phrase_type)) < 1:
115             sys.exit()
116     except SystemExit:
117         LOG.error("Bad class/type for language %s: %s=%s", lang, phrase_class, phrase_type)
118         raise
119
120
121 def _process_amenity(db_connection, phrase_label, phrase_class, phrase_type, phrase_operator):
122     """
123         Add phrase lookup and corresponding class and type to the word table based on the operator.
124     """
125     with db_connection.cursor() as db_cursor:
126         if phrase_operator == 'near':
127             db_cursor.execute("""SELECT getorcreate_amenityoperator(
128                               make_standard_name(%s), %s, %s, 'near')""",
129                               (phrase_label, phrase_class, phrase_type))
130         elif phrase_operator == 'in':
131             db_cursor.execute("""SELECT getorcreate_amenityoperator(
132                               make_standard_name(%s), %s, %s, 'in')""",
133                               (phrase_label, phrase_class, phrase_type))
134         else:
135             db_cursor.execute("""SELECT getorcreate_amenity(
136                               make_standard_name(%s), %s, %s)""",
137                               (phrase_label, phrase_class, phrase_type))
138
139
140 def _create_place_classtype_table_and_indexes(db_connection, config, pairs):
141     """
142         Create table place_classtype for each given pair.
143         Also create indexes on place_id and centroid.
144     """
145     LOG.warning('Create tables and indexes...')
146
147     sql_tablespace = config.TABLESPACE_AUX_DATA
148     if sql_tablespace:
149         sql_tablespace = ' TABLESPACE '+sql_tablespace
150
151     with db_connection.cursor() as db_cursor:
152         db_cursor.execute("CREATE INDEX idx_placex_classtype ON placex (class, type)")
153
154     for _, pair in pairs.items():
155         phrase_class = pair[0]
156         phrase_type = pair[1]
157
158         #Table creation
159         _create_place_classtype_table(
160             db_connection, sql_tablespace, phrase_class, phrase_type
161         )
162
163         #Indexes creation
164         _create_place_classtype_indexes(
165             db_connection, sql_tablespace, phrase_class, phrase_type
166         )
167
168         #Grant access on read to the web user.
169         _grant_access_to_webuser(
170             db_connection, config, phrase_class, phrase_type
171         )
172
173     with db_connection.cursor() as db_cursor:
174         db_cursor.execute("DROP INDEX idx_placex_classtype")
175
176
177 def _create_place_classtype_table(db_connection, sql_tablespace, phrase_class, phrase_type):
178     """
179         Create table place_classtype of the given phrase_class/phrase_type if doesn't exit.
180     """
181     with db_connection.cursor() as db_cursor:
182         db_cursor.execute(SQL(f"""
183                 CREATE TABLE IF NOT EXISTS {{}} {sql_tablespace} 
184                 AS SELECT place_id AS place_id,st_centroid(geometry) AS centroid FROM placex 
185                 WHERE class = {{}} AND type = {{}}""")
186                           .format(Identifier(f'place_classtype_{phrase_class}_{phrase_type}'),
187                                   Literal(phrase_class), Literal(phrase_type)))
188
189
190 def _create_place_classtype_indexes(db_connection, sql_tablespace, phrase_class, phrase_type):
191     """
192         Create indexes on centroid and place_id for the place_classtype table.
193     """
194     #Index on centroid
195     if not db_connection.index_exists(f'idx_place_classtype_{phrase_class}_{phrase_type}_centroid'):
196         with db_connection.cursor() as db_cursor:
197             db_cursor.execute(SQL(f"""
198                     CREATE INDEX {{}} ON {{}} USING GIST (centroid) {sql_tablespace}""")
199                               .format(Identifier(
200                                   f"""idx_place_classtype_{phrase_class}_{phrase_type}_centroid"""),
201                                       Identifier(f'place_classtype_{phrase_class}_{phrase_type}')))
202
203     #Index on place_id
204     if not db_connection.index_exists(f'idx_place_classtype_{phrase_class}_{phrase_type}_place_id'):
205         with db_connection.cursor() as db_cursor:
206             db_cursor.execute(SQL(f"""
207             CREATE INDEX {{}} ON {{}} USING btree(place_id) {sql_tablespace}""")
208                               .format(Identifier(
209                                   f"""idx_place_classtype_{phrase_class}_{phrase_type}_place_id"""),
210                                       Identifier(f'place_classtype_{phrase_class}_{phrase_type}')))
211
212
213 def _grant_access_to_webuser(db_connection, config, phrase_class, phrase_type):
214     """
215         Grant access on read to the table place_classtype for the webuser.
216     """
217     with db_connection.cursor() as db_cursor:
218         db_cursor.execute(SQL("""GRANT SELECT ON {} TO {}""")
219                           .format(Identifier(f'place_classtype_{phrase_class}_{phrase_type}'),
220                                   Identifier(config.DATABASE_WEBUSER)))
221
222 def _convert_php_settings_if_needed(args, file_path):
223     """
224         Convert php settings file of special phrases to json file if it is still in php format.
225     """
226     file, extension = os.path.splitext(file_path)
227     json_file_path = f'{file}.json'
228     if extension == '.php' and not isfile(json_file_path):
229         try:
230             subprocess.run(['/usr/bin/env', 'php', '-Cq',
231                             args.phplib_dir / 'migration/phraseSettingsToJson.php',
232                             file_path], check=True)
233             LOG.warning('special_phrase configuration file has been converted to json.')
234             return json_file_path
235         except subprocess.CalledProcessError:
236             LOG.error('Error while converting %s to json.', file_path)
237             raise
238     else:
239         return json_file_path