diff options
author | Viktor Govako <viktor.govako@gmail.com> | 2016-01-19 12:12:41 +0300 |
---|---|---|
committer | Viktor Govako <viktor.govako@gmail.com> | 2016-01-19 12:12:41 +0300 |
commit | 39518a55cfc2be7cbc5e8336b61ce306f5c539db (patch) | |
tree | 8097e8833a74e450766b79f60cd47acaae54be8f | |
parent | 7091951a9f272e9a15d4e871a1a108178393ee6c (diff) | |
parent | c96f703988215efe52bc726a729d05b194e8ecee (diff) |
Merge pull request #1395 from deathbaba/osm-editor-changesosm-editor
OSM editor changes & iOS integration.
34 files changed, 1245 insertions, 355 deletions
diff --git a/data/categories.txt b/data/categories.txt index 34e710fab4..44d69acd6c 100644 --- a/data/categories.txt +++ b/data/categories.txt @@ -259,7 +259,7 @@ ro:3brutărie|magazin nb:3bakeri|butikk fi:3leipomo|kauppa -shop|shop-convenience|shop-mall|shop-doityourself|shop-florist|shop-butcher|shop-furniture|shop-alcohol|shop-books|shop-shoes|shop-electronics|shop-hardware|shop-jewelry|shop-optician|shop-gift|shop-mobile_phone|shop-beauty|shop-greengrocer|shop-chemist|shop-garden_centre|shop-sports +shop|shop-convenience|shop-mall|shop-doityourself|shop-mobile_phone|shop-chemist|shop-garden_centre en:2shop|U+1F3EA|U+1F3EC ru:2магазин uk:2магазин|крамниця @@ -288,18 +288,424 @@ ro:3magazin nb:3butikk fi:3kauppa +shop-florist +en:florist’s|U+1F337|U+1F338|U+1F339|U+1F33A|U+1F33B|U+1F33C|U+1F490|U+1F33E|2shop +ru:цветочный магазин|цветы|2магазин +fr:fleuriste|2magasin +da:blomsterbutik|2butik +id:tukang bunga|toko bunga|2toko +ko:플로리스트의꽃가게|1쇼핑|가게 +sv:blomsteraffär|2butik +tr:çiçekçi|2mağaza +uk:магазин квітів|2магазин|крамниця +vi:cửa hàng hoa|tiệm hoa|cửa hàng +hu:virágos|virágbolt|2bolt +de:Florist|Blumengeschäft|3Einkaufenladen|Geschäft|2Laden +fi:floristi|kukkakauppa|3kauppa +cs:květinářství|2obchod +it:fiorista|fioraio|2negozio +nb:blomsterhandler|3butikk +zh-Hant:花店|1購物|店鋪|商店|雜貨店|便利商店 +zh-Hans:花店|商店 +th:ร้านดอกไม้|2ร้านค้า +ja:フローリスト/花屋|1買い物|お買い物|ショップ|商店|雑貨 +ro:florărie|3magazin +ar:محل زهور|متجر زهور |متجر|المتجر +sk:kvety|kvetinárstvo|2obchod +es:floristería|tienda de flores|2tienda +pl:kwiaciarnia|3sklep|towary +nl:bloemist|bloemenwinkel|2winkel +pt:floricultura|2loja|compras + +shop-butcher +en:butcher’s|U+1F356|U+1F357|2shop +ru:мясная лавка|мясо|2магазин +fr:boucherie|2magasin +da:slagter|2butik +id:tukang daging|2toko +ko:정육점의|1쇼핑|가게 +sv:slaktare|2butik +tr:kasap|2mağaza +uk:м'ясний магазин|2магазин|крамниця +vi:cửa hàng thịt|cửa hàng +hu:hentes|2bolt +de:Metzgerei|3Einkaufenladen|Geschäft|2Laden +fi:lihakauppias|3kauppa +cs:řeznictví|2obchod +it:macellaio|2negozio +nb:slakter|3butikk +zh-Hant:肉商|1購物|店鋪|商店|雜貨店|便利商店 +zh-Hans:肉商|商店 +th:ร้านขายเนื้อ|2ร้านค้า +ja:肉屋|1買い物|お買い物|ショップ|商店|雑貨 +ro:măcelărie|3magazin +ar:ججزارة|متجر|المتجر +sk:mäsiar|2obchod +es:carnicería|2tienda +pl:rzeźnik|3sklep|towary +nl:slager|2winkel +pt:açougueiro|2loja|compras + +shop-furniture +en:furniture store|2shop +ru:магазин мебели|2магазин +fr:magasin de meubles|2magasin +da:møbelbutik|2butik +id:toko mebel|2toko +ko:가구 상점|1쇼핑|가게 +sv:möbelaffär|2butik +tr:mobilya mağazası|2mağaza +uk:магазин меблів|2магазин|крамниця +vi:cửa hàng nội thất|cửa hàng +hu:bútoráruház|2bolt +de:Möbelgeschäft|3Einkaufenladen|Geschäft|2Laden +fi:huonekalukauppa|3kauppa +cs:nábytek|2obchod +it:negozio di mobili|2negozio +nb:møbelbutikk|3butikk +zh-Hant:家具店|1購物|店鋪|商店|雜貨店|便利商店 +zh-Hans:家具店|商店 +th:ร้านเฟอร์นิเจอร์|2ร้านค้า +ja:家具店|1買い物|お買い物|ショップ|商店|雑貨 +ro:magazin de mobilă|3magazin +ar:معرض أثاث|متجر|المتجر +sk:nábytok|2obchod +es:tienda de muebles|2tienda +pl:sklep meblowy|3sklep|towary +nl:meubelzaak|2winkel +pt:loja de móveis|2loja|compras + +shop-alcohol +en:liquor store|liquor|U+1F377|2shop +ru:магазин алкоголя|4алкоголь|3винный|3вино-водочный|2магазин +fr:magasin de vins et spiritueux|vins et spiritueux|2magasin +da:vinhandel|2butik +id:toko minuman keras|minuman keras|2toko +ko:주류 판매점|1쇼핑|가게 +sv:spritaffär|alkohol|2butik +tr:içki dükkanı|2mağaza +uk:винний магазин|крамниця спиртних напоїв|2магазин|крамниця +vi:cửa hàng rượu|rượu|cửa hàng +hu:italbolt|ital|2bolt +de:Spirituosengeschäft|Spirituosen|3Einkaufenladen|Geschäft|2Laden +fi:alkoholikauppa|alkoholi|3kauppa +cs:obchod s alkoholem|2obchod +it:enoteca|2negozio +nb:vinmonopol|brennevin|3butikk +zh-Hant:烈酒|1購物|店鋪|商店|雜貨店|便利商店 +zh-Hans:烈酒|商店 +th:ร้านขายเหล้า|2ร้านค้า +ja:酒屋|1買い物|お買い物|ショップ|商店|雑貨 +ro:băuturi alcoolice|3magazin +ar:متجر مشروبات كحولية|خمور|متجر|المتجر +sk:liehoviny|alkohol|2obchod +es:licorería|licor|2tienda +pl:monopolowy|3sklep|towary +nl:slijterij|drankhandel|2winkel +pt:loja de bebidas|bebidas alcoólicas|2loja|compras + +shop-books +en:bookstore|U+1F4D6|U+1F4DA|U+1F4D9|U+1F4D8|U+1F4D7|U+1F4D5|2shop +ru:книжный магазин|книги|2магазин +fr:librairie|2magasin +da:boghandel|2butik +id:toko buku|2toko +ko:서점|1쇼핑|가게 +sv:bokaffär|2butik +tr:kitapçı|2mağaza +uk:книгарня|2магазин|крамниця +vi:hiệu sách|cửa hàng +hu:könyvesbolt|2bolt +de:Büchergeschäft|3Einkaufenladen|Geschäft|2Laden +fi:kirjakauppa|3kauppa +cs:knihkupectví|2obchod +it:libreria|2negozio +nb:bokhandel|3butikk +zh-Hant:書店|1購物|店鋪|商店|雜貨店|便利商店 +zh-Hans:书店|商店 +th:ร้านหนังสือ|2ร้านค้า +ja:本屋|1買い物|お買い物|ショップ|商店|雑貨 +ro:librărie|3magazin +ar:مكتبة لبيع الكتب|متجر|المتجر +sk:kníhkupectvo|2obchod +es:librería|2tienda +pl:księgarnia|3sklep|towary +nl:boekwinkel|2winkel +pt:livraria|2loja|compras + +shop-shoes +en:shoe store|U+1F461|U+1F460|U+1F462|U+1F45E|U+1F45F|2shop +ru:магазин обуви|2обувь|2обувной|2магазин +fr:magasin de chaussures|2magasin +da:skobutik|2butik +id:toko sepatu|2toko +ko:신발 가게|1쇼핑|가게 +sv:skobutik|2butik +tr:ayakkabı mağazası|2mağaza +uk:магазин взуття|2магазин|крамниця +vi:cửa hàng giày|cửa hàng +hu:cipőbolt|2bolt +de:Schuhgeschäft|3Einkaufenladen|Geschäft|2Laden +fi:kenkäkauppa|3kauppa +cs:obuv|2obchod +it:calzolaio|2negozio +nb:skobutikk|3butikk +zh-Hant:鞋店|1購物|店鋪|商店|雜貨店|便利商店 +zh-Hans:鞋店|商店 +th:ร้านขายรองเท้า|2ร้านค้า +ja:靴屋|1買い物|お買い物|ショップ|商店|雑貨 +ro:magazin de încălțăminte|3magazin +ar:متجر أحذية|متجر|المتجر +sk:obuvníctvo|2obchod +es:zapatería|2tienda +pl:sklep obuwniczy|3sklep|towary +nl:schoenenwinkel|2winkel +pt:sapataria|loja de calçados|2loja|compras + +shop-electronics +en:electronics|U+1F4F1|U+1F4BB|U+23F0|U+1F4F7|U+1F4F9|U+1F3A5|U+1F4FA|U+1F4FB|U+1F4DF|U+1F4DE|U+260E|U+1F4E0|U+1F4BD|U+1F4BE|U+1F4BF|U+1F4C0|U+1F4FC|U+1F50B|U+1F4E1|2shop +ru:электротехника|2магазин +fr:magasin d'électroménager|2magasin +da:elektronikbutik|2butik +id:elektronik|2toko +ko:전자|1쇼핑|가게 +sv:elektronik|2butik +tr:elektronik mağazası|2mağaza +uk:електротехнічний магазин|2магазин|крамниця +vi:cửa hàng điện|cửa hàng +hu:elektronika|2bolt +de:Elektronik|3Einkaufenladen|Geschäft|2Laden +fi:elektroniikka|3kauppa +cs:elektronika|2obchod +it:negozio di elettronica|2negozio +nb:elektronikk forhandler|3butikk +zh-Hant:電子產品|1購物|店鋪|商店|雜貨店|便利商店 +zh-Hans:电子产品|商店 +th:ร้านขายอุปกรณ์อิเล็กทรอนิกส์|2ร้านค้า +ja:電気店|1買い物|お買い物|ショップ|商店|雑貨 +ro:electronice|3magazin +ar:متجر إلكترونيات|متجر|المتجر +sk:elektronika|2obchod +es:electrónica|2tienda +pl:sklep ze sprzętem elektronicznym|3sklep|towary +nl:elektronicazaak|2winkel +pt:loja de eletrônicos|2loja|compras + +shop-hardware +en:hardware store|U+1F50B|U+1F50C|U+1F4A1|U+1F526|U+1F529|U+1F528|U+2614|2shop +ru:хозяйственный магазин|2магазин +fr:quincaillerie|2magasin +da:isenkræmmer|2butik +id:toko perangkat keras|2toko +ko:철물점|1쇼핑|가게 +sv:järnhandel|2butik +tr:hırdavatçı|2mağaza +uk:магазин побутової техніки|2магазин|крамниця +vi:cửa hàng phần cứng|cửa hàng +hu:vaskereskedés|2bolt +de:Eisenwarengeschäft|3Einkaufenladen|Geschäft|2Laden +fi:työkalukauppa|3kauppa +cs:železářství|2obchod +it:ferramenta|2negozio +nb:jernvareforretning|3butikk +zh-Hant:硬件店|1購物|店鋪|商店|雜貨店|便利商店 +zh-Hans:硬件店|商店 +th:ร้านขายฮาร์ดแวร์|2ร้านค้า +ja:工具店|1買い物|お買い物|ショップ|商店|雑貨 +ro:magazin hardware|3magazin +ar:متجر حواسيب|متجر|المتجر +sk:železiarstvo|2obchod +es:ferretería|2tienda +pl:sklep narzędziowy|3sklep|towary +nl:ijzerhandel|2winkel +pt:loja de ferramentas|2loja|compras + +shop-jewelry +en:jewelry|U+1F48D|2shop +ru:ювелирный магазин|2магазин +fr:bijouterie|2magasin +da:smykkebutik|2butik +id:perhiasan|2toko +ko:보석류|1쇼핑|가게 +sv:smycken|2butik +tr:kuyumcu|2mağaza +uk:ювелірний магазин|2магазин|крамниця +vi:đồ trang sức|cửa hàng +hu:ékszer|2bolt +de:Juweliergeschäft|3Einkaufenladen|Geschäft|2Laden +fi:korukauppa|3kauppa +cs:klenotnictví|2obchod +it:gioielleria|2negozio +nb:gullsmed|3butikk +zh-Hant:珠寶店|1購物|店鋪|商店|雜貨店|便利商店 +zh-Hans:珠宝店|商店 +th:ร้านขายเครื่องประดับ|2ร้านค้า +ja:宝石店|1買い物|お買い物|ショップ|商店|雑貨 +ro:bijutier|3magazin +ar:مجوهرات|المتجر +sk:klenotníctvo|2obchod +es:joyería|2tienda +pl:jubiler|3sklep|towary +nl:juwelier|2winkel +pt:joias|2loja|compras + +shop-optician +en:optician’s|U+1F453|2shop +ru:оптика|2магазин +fr:opticien|2magasin +da:optiker|2butik +id:toko kacamata|2toko +ko:안경점 의|1쇼핑|가게 +sv:optiker|2butik +tr:gözlükçü|2mağaza +uk:оптика|2магазин|крамниця +vi:cửa hàng mắt kính|cửa hàng +hu:optika|2bolt +de:Optiker|3Einkaufenladen|Geschäft|2Laden +fi:optikko|3kauppa +cs:optika|2obchod +it:ottico|2negozio +nb:optiker|3butikk +zh-Hant:眼鏡店|1購物|店鋪|商店|雜貨店|便利商店 +zh-Hans:眼镜店|商店 +th:ร้านแว่น|2ร้านค้า +ja:眼鏡店|1買い物|お買い物|ショップ|商店|雑貨 +ro:optică|3magazin +ar:مركز بصريات|متجر|المتجر +sk:optika|2obchod +es:óptica|2tienda +pl:sklep optyczny|3sklep|towary +nl:opticien|2winkel +pt:oculista|2loja|compras + +shop-gift +en:gift shop|U+1F381|2shop +ru:сувениры|2магазин +fr:boutique de souvenirs|2magasin +da:gavebutik|2butik +id:toko hadiah|2toko +ko:선물 가게|1쇼핑|가게 +sv:presentaffär|2butik +tr:hediyelik eşya mağazası|2mağaza +uk:магазин сувенірів|2магазин|крамниця +vi:cửa hàng quà tặng|cửa hàng +hu:ajándékbolt|2bolt +de:Geschenkladen|3Einkaufenladen|Geschäft|2Laden +fi:lahjakauppa|3kauppa +cs:obchod s dárkovým zbožím|2obchod +it:negozio di regali|2negozio +nb:gavebutikk|3butikk +zh-Hant:禮品店|1購物|店鋪|商店|雜貨店|便利商店 +zh-Hans:礼品店|商店 +th:ร้านของขวัญ|2ร้านค้า +ja:ギフトショップ|1買い物|お買い物|ショップ|商店|雑貨 +ro:magazin de suveniruri|3magazin +ar:متجر هدايا|متجر|المتجر +sk:darčeky|2obchod +es:tienda de regalos|2tienda +pl:sklep pamiątkarski|3sklep|towary +nl:cadeauwinkel|2winkel +pt:loja de presentes|2loja|compras + +shop-beauty +en:beauty shop|U+1F484|2shop +ru:магазин косметики|косметика|2магазин +fr:institut de beauté|2magasin +da:skønhedsbutik|2butik +id:toko barang kecantikan|2toko +ko:미용 용품 가게|1쇼핑|가게 +sv:skönhetsbutik|2butik +tr:kozmetik ürünler mağazası|2mağaza +uk:магазин товарів для краси|2магазин|крамниця +vi:cửa hàng làm đẹp|cửa hàng +hu:kozmetikai termékek|2bolt +de:Kosmetikgeschäft|3Einkaufenladen|Geschäft|2Laden +fi:kauneusliike|3kauppa +cs:kosmetický salón|2obchod +it:estetista|2negozio +nb:skjønnhetssalong|3butikk +zh-Hant:美容產品店|1購物|店鋪|商店|雜貨店|便利商店 +zh-Hans:美容产品店|商店 +th:ร้านขายผลิตภัณฑ์ความงาม|2ร้านค้า +ja:美容品店|1買い物|お買い物|ショップ|商店|雑貨 +ro:magazin de cosmetice|3magazin +ar:مركز أدوات تجميل|متجر|المتجر +sk:drogéria|2obchod +es:tienda de productos de belleza|2tienda +pl:sklep kosmetyczny|3sklep|towary +nl:winkel voor schoonheidsproducten|2winkel +pt:loja de cosméticos|2loja|compras + +shop-greengrocer +en:4greengrocer's|greengrocers|greengrocers'|3grocery|U+1F345|U+1F346|U+1F33D|U+1F360|U+1F348|U+1F347|U+1F349|U+1F34A|U+1F34C|U+1F34D|U+1F34E|U+1F34F|U+1F350|U+1F351|U+1F353|2shop +ru:овощи и фрукты|овощи|фрукты|2магазин +fr:primeur|2magasin +da:grønthandler|2butik +id:penjual sayuran|2toko +ko:청과물 상인의|1쇼핑|가게 +sv:grönsakshandlare|2butik +tr:manav|2mağaza +uk:магазин овочів|овочі|фрукти|2магазин|крамниця +vi:cửa hàng rau củ|cửa hàng +hu:zöldséges|2bolt +de:Gemüseladen|3Einkaufenladen|Geschäft|2Laden +fi:vihanneskauppias|3kauppa +cs:ovoce a zelenina|2obchod +it:fruttivendolo|2negozio +nb:frukt- og grønnsakshandler|3butikk +zh-Hant:蔬果零售店|1購物|店鋪|商店|雜貨店|便利商店 +zh-Hans:蔬果零售店|商店 +th:ร้านขายผัด|2ร้านค้า +ja:八百屋|1買い物|お買い物|ショップ|商店|雑貨 +ro:băcănie|3magazin +ar:محل خضراوات|متجر|المتجر +sk:zelovoc|2obchod +es:frutería|2tienda +pl:warzywniak|3sklep|towary +nl:groenteboer|2winkel +pt:quitanda|2loja|compras + +shop-sports +en:sports goods|U+1F3BF|U+1F3A3|U+1F3C2|U+1F6B4|U+26BD|U+1F3C0|U+1F3C8|U+26BE|U+1F3BE|U+1F3C9|U+26F3|2shop +ru:спорттовары|товары для спорта|2магазин +fr:articles de sport|2magasin +da:sportsudstyr|2butik +id:barang olahraga|2toko +ko:스포츠 용품|1쇼핑|가게 +sv:sportaffär|2butik +tr:spor ürünleri|2mağaza +uk:спортивні товари|2магазин|крамниця +vi:đồ dùng thể thao|cửa hàng +hu:sporteszközök|2bolt +de:Sportartikel|3Einkaufenladen|Geschäft|2Laden +fi:urheilukauppa|3kauppa +cs:sportovní zboží|2obchod +it:negozio sportivo|negozio articoli sportivi|2negozio +nb:sportsutstyr|3butikk +zh-Hant:運動商品店|1購物|店鋪|商店|雜貨店|便利商店 +zh-Hans:运动商品店|商店 +th:สินค้ากีฬา|2ร้านค้า +ja:スポーツ用品店|1買い物|お買い物|ショップ|商店|雑貨 +ro:articole sportive|3magazin +ar:أدوات رياضية|متجر|المتجر +sk:športové potreby|2obchod +es:productos de deporte|2tienda +pl:sklep sportowy|3sklep|towary +nl:sportartikelen|2winkel +pt:artigos esportivos|2loja|compras + shop-supermarket|shop-department_store en:3supermarket|shop|U+1F3EA|U+1F3EC ru:3универсам|3супермаркет|магазин uk:3супермаркет|3універсам|2магазин|крамниця de:3Supermarkt|Laden -fr:3supermarché|magasin +fr:3supermarché|2magasin it:3supermercato|negozio es:3supermercado|tienda ko:1슈퍼마켓|쇼핑|가게 ja:1スーパーマーケット|ショップ|お買い物|買い物|商店 -cs:3supermarket|obchod -sk:3supermarket|obchod +cs:3supermarket|2obchod +sk:3supermarket|2obchod nl:3supermarkt|winkel zh-Hant:1超級市場|市場|購物 pl:4supermarket|zakupy|sklep @@ -1725,7 +2131,7 @@ zh-Hant:1垃圾桶|垃圾|回收|回收場 pl:3recykling|ponowne odtworzenie|odpady pt:reciclagem|lixo hu:szemetes -th: การรีไซเคิล|ถังขยะ|ขยะ +th:การรีไซเคิล|ถังขยะ|ขยะ zh-Hans:1回收 ar:تدوير da:genbrug|skrald @@ -2073,7 +2479,7 @@ zh-Hant:省|州 pl:3stan|4region|5prowincja pt:estado|província hu:állam -th: รัฐ +th:รัฐ zh-Hans:州|省 ar:محافظة da:stat|provins @@ -2102,7 +2508,7 @@ zh-Hant:1地區 pl:4region pt:região hu:régió -th: ภูมิภาค +th:ภูมิภาค zh-Hans:区域 ar:منطقة da:region @@ -2706,7 +3112,7 @@ sk:penzión|hotel|hostel|ubytovňa nl:gasthuis|hotel|hostel zh-Hant:1賓館|旅館|飯店|酒店|旅舍|住宿|招待所 pl:4pensjonat|hotel|hostel|gościnne pokoje -pt: casa de hóspedes|pousada|motel +pt:casa de hóspedes|pousada|motel hu:vendégház|hotel|szálloda th:3เกสท์เฮ้าส์|โรงแรม์|์โรงแรม zh-Hans:1招待所์|์旅馆์|์旅社|旅店 @@ -2953,33 +3359,33 @@ nb:golfbane fi:golf-rata leisure-pitch -en:pitch|sport|U+26BD|U+26BE|U+1F3BE|U+1F3C0|U+1F3C8|U+1F3C9|U+1F3C3 +en:sports ground|sport|U+26BD|U+26BE|U+1F3BE|U+1F3C0|U+1F3C8|U+1F3C9|U+1F3C3 ru:спортплощадка|спорт uk:спортмайданчик|спорт -de:Feld|Sport +de:Sportplatz|Feld|Sport fr:terrain de sport|sport -it:campo|sport -es:terreno|deporte -ko:스포츠 경내|스포츠 +it:campo sportivo|campo|sport +es:complejo deportivo|terreno|deporte +ko:경기장|스포츠 경내|스포츠 ja:1運動場|スポーツ|トラック|球場 -cs:sport -sk:šport -nl:veld|sport +cs:sportovní hřiště|sport +sk:športovisko|šport +nl:sportveld|veld|sport zh-Hant:2運動場|體育館|球場|2足球場|體育|運動|健身 -pl:pole|boisko|sport -pt:campo|desporto -hu:oszlop|sport -th:ขว้าง|กีฬา +pl:boisko sportowe|pole|boisko|sport +pt:campo de esportes|campo|desporto +hu:sportpálya|oszlop|sport +th:พื้นสนามกีฬา|ขว้าง|กีฬา zh-Hans:2球场|运动 -ar:ميدان اللعب|رياضة -da:sportsplads|fiskested|fodboldbane|bane -tr:alan|spor -sv:bana|sport -vi:đường pích -id:lapangan -ro:gazon +ar:ميدان اللعب|رياضة |ملاعب رياضية +da:teltplads|fiskested|fodboldbane|bane|sportsplads +tr:spor sahası|alan|spor +sv:idrottsplats|bana|sport +vi:sân vận động|đường pích +id:lapangan olahraga|lapangan +ro:teren de sport|gazon nb:sportssenter -fi:nimikkopaikka +fi:urheilukenttä|nimikkopaikka leisure-swimming_pool en:4swimming pool|sport|U+1F3CA diff --git a/editor/changeset_wrapper.cpp b/editor/changeset_wrapper.cpp new file mode 100644 index 0000000000..fe2fff6df4 --- /dev/null +++ b/editor/changeset_wrapper.cpp @@ -0,0 +1,120 @@ +#include "indexer/feature.hpp" + +#include "editor/changeset_wrapper.hpp" + +#include "std/algorithm.hpp" +#include "std/sstream.hpp" + +#include "private.h" + +using editor::XMLFeature; + +string DebugPrint(pugi::xml_document const & doc) +{ + ostringstream stream; + doc.print(stream, " "); + return stream.str(); +} + +namespace osm +{ + +ChangesetWrapper::ChangesetWrapper(TKeySecret const & keySecret, + ServerApi06::TKeyValueTags const & comments) + : m_changesetComments(comments), + m_api(OsmOAuth::ServerAuth().SetToken(keySecret)) +{ +} + +ChangesetWrapper::~ChangesetWrapper() +{ + if (m_changesetId) + m_api.CloseChangeSet(m_changesetId); +} + +void ChangesetWrapper::LoadXmlFromOSM(ms::LatLon const & ll, pugi::xml_document & doc) +{ + auto const response = m_api.GetXmlFeaturesAtLatLon(ll.lat, ll.lon); + if (response.first == OsmOAuth::ResponseCode::NetworkError) + MYTHROW(NetworkErrorException, ("NetworkError with GetXmlFeaturesAtLatLon request.")); + if (response.first != OsmOAuth::ResponseCode::OK) + MYTHROW(HttpErrorException, ("HTTP error", response.first, "with GetXmlFeaturesAtLatLon", ll)); + + if (pugi::status_ok != doc.load(response.second.c_str()).status) + MYTHROW(OsmXmlParseException, ("Can't parse OSM server response for GetXmlFeaturesAtLatLon request", response.second)); +} + +XMLFeature ChangesetWrapper::GetMatchingFeatureFromOSM(XMLFeature const & ourPatch, FeatureType const & feature) +{ + if (feature.GetFeatureType() == feature::EGeomType::GEOM_POINT) + { + // Match with OSM node. + ms::LatLon const ll = ourPatch.GetCenter(); + pugi::xml_document doc; + // Throws! + LoadXmlFromOSM(ll, doc); + + // TODO(AlexZ): Select best matching OSM node, not just the first one. + pugi::xml_node const firstNode = doc.child("osm").child("node"); + if (firstNode.empty()) + MYTHROW(OsmObjectWasDeletedException, ("OSM does not have any nodes at the coordinates", ll, ", server has returned:", doc)); + + return XMLFeature(firstNode); + } + else if (feature.GetFeatureType() == feature::EGeomType::GEOM_AREA) + { + using m2::PointD; + // Set filters out duplicate points for closed ways or triangles' vertices. + set<PointD> geometry; + feature.ForEachTriangle([&geometry](PointD const & p1, PointD const & p2, PointD const & p3) + { + geometry.insert(p1); + geometry.insert(p2); + geometry.insert(p3); + }, FeatureType::BEST_GEOMETRY); + + ASSERT_GREATER_OR_EQUAL(geometry.size(), 3, ("Is it an area feature?")); + + for (auto const & pt : geometry) + { + ms::LatLon const ll = MercatorBounds::ToLatLon(pt); + pugi::xml_document doc; + // Throws! + LoadXmlFromOSM(ll, doc); + + // TODO(AlexZ): Select best matching OSM way from possible many ways. + pugi::xml_node const firstWay = doc.child("osm").child("way"); + if (firstWay.empty()) + continue; + + XMLFeature const way(firstWay); + if (!way.IsArea()) + continue; + + // TODO: Check that this way is really match our feature. + + return way; + } + MYTHROW(OsmObjectWasDeletedException, ("OSM does not have any matching way for feature", feature)); + } + MYTHROW(LinearFeaturesAreNotSupportedException, ("We don't edit linear features yet.")); +} + +void ChangesetWrapper::ModifyNode(XMLFeature node) +{ + // TODO(AlexZ): ServerApi can be much better with exceptions. + if (m_changesetId == kInvalidChangesetId && !m_api.CreateChangeSet(m_changesetComments, m_changesetId)) + MYTHROW(CreateChangeSetFailedException, ("CreateChangeSetFailedException")); + + uint64_t nodeId; + if (!strings::to_uint64(node.GetAttribute("id"), nodeId)) + MYTHROW(CreateChangeSetFailedException, ("CreateChangeSetFailedException")); + + // Changeset id should be updated for every OSM server commit. + node.SetAttribute("changeset", strings::to_string(m_changesetId)); + + if (!m_api.ModifyNode(node.ToOSMString(), nodeId)) + MYTHROW(ModifyNodeFailedException, ("ModifyNodeFailedException")); +} + +} // namespace osm diff --git a/editor/changeset_wrapper.hpp b/editor/changeset_wrapper.hpp new file mode 100644 index 0000000000..07d4696bd0 --- /dev/null +++ b/editor/changeset_wrapper.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include "editor/server_api.hpp" +#include "editor/xml_feature.hpp" + +#include "base/exception.hpp" + +class FeatureType; + +namespace osm +{ + +struct ClientToken; + +class ChangesetWrapper +{ +public: + DECLARE_EXCEPTION(ChangesetWrapperException, RootException); + DECLARE_EXCEPTION(NetworkErrorException, ChangesetWrapperException); + DECLARE_EXCEPTION(HttpErrorException, ChangesetWrapperException); + DECLARE_EXCEPTION(OsmXmlParseException, ChangesetWrapperException); + DECLARE_EXCEPTION(OsmObjectWasDeletedException, ChangesetWrapperException); + DECLARE_EXCEPTION(CreateChangeSetFailedException, ChangesetWrapperException); + DECLARE_EXCEPTION(ModifyNodeFailedException, ChangesetWrapperException); + DECLARE_EXCEPTION(LinearFeaturesAreNotSupportedException, ChangesetWrapperException); + + ChangesetWrapper(TKeySecret const & keySecret, ServerApi06::TKeyValueTags const & comments); + ~ChangesetWrapper(); + + /// Throws many exceptions from above list, plus including XMLNode's parsing ones. + /// OsmObjectWasDeletedException means that node was deleted from OSM server by someone else. + editor::XMLFeature GetMatchingFeatureFromOSM(editor::XMLFeature const & ourPatch, FeatureType const & feature); + + /// Throws exceptions from above list. + void ModifyNode(editor::XMLFeature node); + +private: + /// Unfortunately, pugi can't return xml_documents from methods. + /// Throws exceptions from above list. + void LoadXmlFromOSM(ms::LatLon const & ll, pugi::xml_document & doc); + + ServerApi06::TKeyValueTags m_changesetComments; + ServerApi06 m_api; + static constexpr uint64_t kInvalidChangesetId = 0; + uint64_t m_changesetId = kInvalidChangesetId; +}; + +} // namespace osm diff --git a/editor/editor.pro b/editor/editor.pro index ce3506fdbe..03db84f275 100644 --- a/editor/editor.pro +++ b/editor/editor.pro @@ -9,15 +9,17 @@ ROOT_DIR = .. include($$ROOT_DIR/common.pri) SOURCES += \ + changeset_wrapper.cpp \ opening_hours_ui.cpp \ + osm_auth.cpp \ server_api.cpp \ ui2oh.cpp \ xml_feature.cpp \ - osm_auth.cpp \ HEADERS += \ + changeset_wrapper.hpp \ opening_hours_ui.hpp \ + osm_auth.hpp \ server_api.hpp \ ui2oh.hpp \ xml_feature.hpp \ - osm_auth.hpp \ diff --git a/editor/editor_tests/server_api_test.cpp b/editor/editor_tests/server_api_test.cpp index 42e7ec4126..43d4bb6bbe 100644 --- a/editor/editor_tests/server_api_test.cpp +++ b/editor/editor_tests/server_api_test.cpp @@ -116,7 +116,7 @@ UNIT_TEST(OSM_ServerAPI_ChangesetActions) // New changeset has new id. TEST(SetAttributeForOsmNode(node, "changeset", changeSetId), ()); - auto const response = api.GetXmlNodeByLatLon(node.child("osm").child("node").attribute("lat").as_double(), + auto const response = api.GetXmlFeaturesAtLatLon(node.child("osm").child("node").attribute("lat").as_double(), node.child("osm").child("node").attribute("lon").as_double()); TEST_EQUAL(response.first, OsmOAuth::ResponseCode::OK, ()); xml_document reply; diff --git a/editor/editor_tests/xml_feature_test.cpp b/editor/editor_tests/xml_feature_test.cpp index c18d2b8b53..450ea99117 100644 --- a/editor/editor_tests/xml_feature_test.cpp +++ b/editor/editor_tests/xml_feature_test.cpp @@ -93,6 +93,48 @@ UNIT_TEST(XMLFeature_ToOSMString) TEST_EQUAL(expectedString, feature.ToOSMString(), ()); } +UNIT_TEST(XMLFeature_IsArea) +{ + constexpr char const * validAreaXml = R"( +<way timestamp="2015-11-27T21:13:32Z"> + <nd ref="822403"/> + <nd ref="21533912"/> + <nd ref="821601"/> + <nd ref="822403"/> +</way> +)"; + TEST(XMLFeature(validAreaXml).IsArea(), ()); + + constexpr char const * notClosedWayXml = R"( +<way timestamp="2015-11-27T21:13:32Z"> + <nd ref="822403"/> + <nd ref="21533912"/> + <nd ref="821601"/> + <nd ref="123321"/> +</way> +)"; + TEST(!XMLFeature(notClosedWayXml).IsArea(), ()); + + constexpr char const * invalidWayXml = R"( +<way timestamp="2015-11-27T21:13:32Z"> + <nd ref="822403"/> + <nd ref="21533912"/> + <nd ref="822403"/> +</way> +)"; + TEST(!XMLFeature(invalidWayXml).IsArea(), ()); + + constexpr char const * emptyWay = R"( +<way timestamp="2015-11-27T21:13:32Z"/> +)"; + TEST(!XMLFeature(emptyWay).IsArea(), ()); + + constexpr char const * node = R"( +<node lat="0.0" lon="0.0" timestamp="2015-11-27T21:13:32Z"/> +)"; + TEST(!XMLFeature(node).IsArea(), ()); +} + // UNIT_TEST(XMLFeature_FromXml) // { // auto const srcString = R"(<?xml version="1.0"?> diff --git a/editor/osm_auth.cpp b/editor/osm_auth.cpp index d381884311..7ce5af73a4 100644 --- a/editor/osm_auth.cpp +++ b/editor/osm_auth.cpp @@ -91,6 +91,12 @@ OsmOAuth OsmOAuth::ProductionServerAuth() return OsmOAuth(OSM_CONSUMER_KEY, OSM_CONSUMER_SECRET, kOsmMainSiteURL, kOsmApiURL); } +OsmOAuth OsmOAuth::ServerAuth() +{ + // TODO(AlexZ): Replace with ProductionServerAuth before release. + return IZServerAuth(); +} + // Opens a login page and extract a cookie and a secret token. OsmOAuth::AuthResult OsmOAuth::FetchSessionId(OsmOAuth::SessionID & sid) const { diff --git a/editor/osm_auth.hpp b/editor/osm_auth.hpp index aef959a009..4665c19b25 100644 --- a/editor/osm_auth.hpp +++ b/editor/osm_auth.hpp @@ -56,6 +56,8 @@ public: static OsmOAuth DevServerAuth(); /// api.openstreetmap.org static OsmOAuth ProductionServerAuth(); + /// Should be used everywhere in production code. + static OsmOAuth ServerAuth(); /// @name Stateless methods. //@{ diff --git a/editor/server_api.cpp b/editor/server_api.cpp index 10d04192df..162c0a36a7 100644 --- a/editor/server_api.cpp +++ b/editor/server_api.cpp @@ -114,7 +114,7 @@ OsmOAuth::Response ServerApi06::GetXmlFeaturesInRect(m2::RectD const & latLonRec return m_auth.DirectRequest(url); } -OsmOAuth::Response ServerApi06::GetXmlNodeByLatLon(double lat, double lon) const +OsmOAuth::Response ServerApi06::GetXmlFeaturesAtLatLon(double lat, double lon) const { double const kInflateEpsilon = MercatorBounds::GetCellID2PointAbsEpsilon(); m2::RectD rect(lon, lat, lon, lat); diff --git a/editor/server_api.hpp b/editor/server_api.hpp index db6b0d74dd..b0de50fdfa 100644 --- a/editor/server_api.hpp +++ b/editor/server_api.hpp @@ -42,7 +42,7 @@ public: /// @returns OSM xml string with features in the bounding box or empty string on error. OsmOAuth::Response GetXmlFeaturesInRect(m2::RectD const & latLonRect) const; - OsmOAuth::Response GetXmlNodeByLatLon(double lat, double lon) const; + OsmOAuth::Response GetXmlFeaturesAtLatLon(double lat, double lon) const; private: OsmOAuth m_auth; diff --git a/editor/xml_feature.cpp b/editor/xml_feature.cpp index 27bb8f2966..57ad1dd873 100644 --- a/editor/xml_feature.cpp +++ b/editor/xml_feature.cpp @@ -16,7 +16,7 @@ namespace { constexpr int const kLatLonTolerance = 7; constexpr char const * kTimestamp = "timestamp"; -constexpr char const * kOffset = "offset"; +constexpr char const * kIndex = "mwm_file_index"; constexpr char const * kUploadTimestamp = "upload_timestamp"; constexpr char const * kUploadStatus = "upload_status"; constexpr char const * kUploadError = "upload_error"; @@ -112,6 +112,21 @@ XMLFeature::Type XMLFeature::GetType() const return strcmp(GetRootNode().name(), "node") == 0 ? Type::Node : Type::Way; } +bool XMLFeature::IsArea() const +{ + if (strcmp(GetRootNode().name(), kWayType) != 0) + return false; + + vector<string> ndIds; + for (auto const & nd : GetRootNode().select_nodes("nd")) + ndIds.push_back(nd.node().attribute("ref").value()); + + if (ndIds.size() < 4) + return false; + + return ndIds.front() == ndIds.back(); +} + void XMLFeature::Save(ostream & ost) const { m_document.save(ost, " "); @@ -197,15 +212,15 @@ void XMLFeature::SetModificationTime(time_t const time) SetAttribute(kTimestamp, my::TimestampToString(time)); } -uint32_t XMLFeature::GetOffset() const +uint32_t XMLFeature::GetMWMFeatureIndex() const { // Always cast to uint32_t to avoid warnings on different platforms. - return static_cast<uint32_t>(GetRootNode().attribute(kOffset).as_uint(0)); + return static_cast<uint32_t>(GetRootNode().attribute(kIndex).as_uint(0)); } -void XMLFeature::SetOffset(uint32_t featureOffset) +void XMLFeature::SetMWMFeatureIndex(uint32_t index) { - SetAttribute(kOffset, strings::to_string(featureOffset)); + SetAttribute(kIndex, strings::to_string(index)); } time_t XMLFeature::GetUploadTime() const diff --git a/editor/xml_feature.hpp b/editor/xml_feature.hpp index 3821be7b5e..f2d59784e6 100644 --- a/editor/xml_feature.hpp +++ b/editor/xml_feature.hpp @@ -49,6 +49,9 @@ public: Type GetType() const; + /// @returns true only if it is a way and it is closed (area). + bool IsArea() const; + ms::LatLon GetCenter() const; void SetCenter(m2::PointD const & mercatorCenter); @@ -84,8 +87,8 @@ public: /// @name XML storage format helpers. //@{ - uint32_t GetOffset() const; - void SetOffset(uint32_t featureOffset); + uint32_t GetMWMFeatureIndex() const; + void SetMWMFeatureIndex(uint32_t index); /// @returns my::INVALID_TIME_STAMP if there were no any upload attempt. time_t GetUploadTime() const; diff --git a/indexer/feature.cpp b/indexer/feature.cpp index 326c54fc3e..f712d1215b 100644 --- a/indexer/feature.cpp +++ b/indexer/feature.cpp @@ -266,6 +266,11 @@ void FeatureType::ParseMetadata() const m_bMetadataParsed = true; } +StringUtf8Multilang const & FeatureType::GetNames() const +{ + return m_params.name; +} + void FeatureType::SetNames(StringUtf8Multilang const & newNames) { m_params.name.Clear(); @@ -466,6 +471,14 @@ string FeatureType::GetHouseNumber() const return m_params.house.Get(); } +void FeatureType::SetHouseNumber(string const & number) +{ + if (number.empty()) + m_params.house.Clear(); + else + m_params.house.Set(number); +} + bool FeatureType::GetName(int8_t lang, string & name) const { if (!HasName()) diff --git a/indexer/feature.hpp b/indexer/feature.hpp index 3598ea1701..9c1ef8bf83 100644 --- a/indexer/feature.hpp +++ b/indexer/feature.hpp @@ -167,6 +167,7 @@ public: /// @name Editor functions. //@{ + StringUtf8Multilang const & GetNames() const; void SetNames(StringUtf8Multilang const & newNames); void SetMetadata(feature::Metadata const & newMetadata); //@} @@ -261,6 +262,8 @@ public: friend string DebugPrint(FeatureType const & ft); string GetHouseNumber() const; + /// Needed for Editor, to change house numbers in runtime. + void SetHouseNumber(string const & number); /// @name Get names for feature. /// @param[out] defaultName corresponds to osm tag "name" diff --git a/indexer/ftypes_matcher.cpp b/indexer/ftypes_matcher.cpp index 11e53c4083..a9b9ee2ad3 100644 --- a/indexer/ftypes_matcher.cpp +++ b/indexer/ftypes_matcher.cpp @@ -45,6 +45,11 @@ bool BaseChecker::operator() (vector<uint32_t> const & types) const return false; } +bool BaseChecker::HasTypeValue(uint32_t const type) const +{ + return find(m_types.begin(), m_types.end(), type) != m_types.end(); +} + IsPeakChecker::IsPeakChecker() { Classificator const & c = classif(); diff --git a/indexer/ftypes_matcher.hpp b/indexer/ftypes_matcher.hpp index f6a745017c..a8e563e7ad 100644 --- a/indexer/ftypes_matcher.hpp +++ b/indexer/ftypes_matcher.hpp @@ -28,6 +28,8 @@ public: bool operator() (feature::TypesHolder const & types) const; bool operator() (FeatureType const & ft) const; bool operator() (vector<uint32_t> const & types) const; + // Simple type equality comparison. No magic like in IsMatched. + bool HasTypeValue(uint32_t const type) const; static uint32_t PrepareToMatch(uint32_t type, uint8_t level); }; diff --git a/indexer/index.cpp b/indexer/index.cpp index 79f7f63398..3110b8a08a 100644 --- a/indexer/index.cpp +++ b/indexer/index.cpp @@ -123,8 +123,11 @@ void Index::FeaturesLoaderGuard::GetFeatureByIndex(uint32_t index, FeatureType & ASSERT_NOT_EQUAL(osm::Editor::FeatureStatus::Deleted, m_editor.GetFeatureStatus(id, index), ("Deleted feature was cached. Please review your code.")); if (!m_editor.Instance().GetEditedFeature(id, index, ft)) - { - m_vector.GetByIndex(index, ft); - ft.SetID(FeatureID(id, index)); - } + GetOriginalFeatureByIndex(index, ft); +} + +void Index::FeaturesLoaderGuard::GetOriginalFeatureByIndex(uint32_t index, FeatureType & ft) const +{ + m_vector.GetByIndex(index, ft); + ft.SetID(FeatureID(m_handle.GetId(), index)); } diff --git a/indexer/index.hpp b/indexer/index.hpp index 08b1c9474f..8694055151 100644 --- a/indexer/index.hpp +++ b/indexer/index.hpp @@ -284,7 +284,10 @@ public: inline MwmSet::MwmId const & GetId() const { return m_handle.GetId(); } string GetCountryFileName() const; bool IsWorld() const; + /// Everyone, except Editor core, should use this method. void GetFeatureByIndex(uint32_t index, FeatureType & ft) const; + /// Editor core only method, to get 'untouched', original version of feature. + void GetOriginalFeatureByIndex(uint32_t index, FeatureType & ft) const; inline FeaturesVector const & GetFeaturesVector() const { return m_vector; } private: diff --git a/indexer/osm_editor.cpp b/indexer/osm_editor.cpp index 3fedbf7844..b24ceb0e28 100644 --- a/indexer/osm_editor.cpp +++ b/indexer/osm_editor.cpp @@ -1,18 +1,25 @@ #include "indexer/classificator.hpp" #include "indexer/feature_decl.hpp" +#include "indexer/feature_impl.hpp" #include "indexer/feature_meta.hpp" +#include "indexer/ftypes_matcher.hpp" #include "indexer/index.hpp" #include "indexer/osm_editor.hpp" #include "platform/platform.hpp" +#include "editor/changeset_wrapper.hpp" +#include "editor/osm_auth.hpp" +#include "editor/server_api.hpp" #include "editor/xml_feature.hpp" +#include "coding/internal/file_data.hpp" + #include "base/logging.hpp" #include "base/string_utils.hpp" -#include "coding/internal/file_data.hpp" - +#include "std/chrono.hpp" +#include "std/future.hpp" #include "std/tuple.hpp" #include "std/unordered_map.hpp" #include "std/unordered_set.hpp" @@ -30,6 +37,12 @@ constexpr char const * kXmlMwmNode = "mwm"; constexpr char const * kDeleteSection = "delete"; constexpr char const * kModifySection = "modify"; constexpr char const * kCreateSection = "create"; +/// We store edited streets in OSM-compatible way. +constexpr char const * kAddrStreetTag = "addr:street"; + +constexpr char const * kUploaded = "Uploaded"; +constexpr char const * kDeletedFromOSMServer = "Deleted from OSM by someone"; +constexpr char const * kNeedsRetry = "Needs Retry"; namespace osm { @@ -43,7 +56,7 @@ string GetEditorFilePath() { return GetPlatform().WritablePathForFile(kEditorXML /// type:string -> description:pair<fields:vector<???>, editName:bool, editAddr:bool> using EType = feature::Metadata::EType; -using TEditableFields = set<EType>; +using TEditableFields = vector<EType>; struct TypeDescription { @@ -56,76 +69,77 @@ struct TypeDescription TEditableFields const fields; bool const name; + // Address == true implies Street, House Number, Phone, Fax, Opening Hours, Website, EMail, Postcode. bool const address; }; static unordered_map<string, TypeDescription> const gEditableTypes = { - {"aeroway-aerodrome", {{EType::FMD_ELE, EType::FMD_PHONE_NUMBER, EType::FMD_OPERATOR}, false, true}}, - {"aeroway-airport", {{EType::FMD_ELE, EType::FMD_PHONE_NUMBER, EType::FMD_OPERATOR}, false, true}}, - {"amenity-atm", {{}, true, false}}, - {"amenity-bank", {{EType::FMD_OPERATOR, EType::FMD_OPEN_HOURS, EType::FMD_WEBSITE, EType::FMD_OPERATOR}, true, true}}, - {"amenity-bar", {{EType::FMD_OPEN_HOURS}, true, true}}, - {"amenity-bicycle_rental", {{EType::FMD_OPERATOR}, false, true}}, - {"amenity-bureau_de_change", {{EType::FMD_OPEN_HOURS}, true, true}}, - {"amenity-bus_station", {{EType::FMD_OPERATOR}, true, false}}, - {"amenity-cafe", {{EType::FMD_OPEN_HOURS, EType::FMD_PHONE_NUMBER, EType::FMD_WEBSITE, EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, - {"amenity-car_rental", {{EType::FMD_OPERATOR}, true, false}}, + {"aeroway-aerodrome", {{EType::FMD_ELE, EType::FMD_OPERATOR}, false, true}}, + {"aeroway-airport", {{EType::FMD_ELE, EType::FMD_OPERATOR}, false, true}}, + {"amenity-atm", {{EType::FMD_OPERATOR, EType::FMD_WEBSITE}, true, false}}, + {"amenity-bank", {{EType::FMD_OPERATOR}, true, true}}, + {"amenity-bar", {{EType::FMD_CUISINE, EType::FMD_INTERNET}, true, true}}, + {"amenity-bicycle_rental", {{EType::FMD_OPERATOR}, true, false}}, + {"amenity-bureau_de_change", {{EType::FMD_OPERATOR}, true, true}}, + {"amenity-bus_station", {{EType::FMD_OPERATOR}, true, true}}, + {"amenity-cafe", {{EType::FMD_CUISINE, EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, + {"amenity-car_rental", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, {"amenity-car_sharing", {{EType::FMD_OPERATOR, EType::FMD_WEBSITE}, true, false}}, - {"amenity-casino", {{EType::FMD_OPERATOR, EType::FMD_WEBSITE, EType::FMD_OPEN_HOURS}, true, false}}, - {"amenity-cinema", {{EType::FMD_OPERATOR, EType::FMD_PHONE_NUMBER, EType::FMD_WEBSITE}, true, true}}, - {"amenity-college", {{EType::FMD_OPERATOR, EType::FMD_WEBSITE, EType::FMD_WEBSITE}, true, true}}, - {"amenity-doctors", {{}, true, true}}, - {"amenity-drinking_water", {{EType::FMD_OPERATOR}, true, false}}, - {"amenity-embassy", {{EType::FMD_PHONE_NUMBER, EType::FMD_WEBSITE}, true, false}}, - {"amenity-fast_food", {{EType::FMD_OPERATOR, EType::FMD_CUISINE}, true, false}}, - {"amenity-ferry_terminal", {{EType::FMD_OPERATOR}, true, false}}, - {"amenity-fire_station", {{}, true, false}}, + {"amenity-casino", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, + {"amenity-cinema", {{EType::FMD_OPERATOR}, true, true}}, + {"amenity-college", {{EType::FMD_OPERATOR}, true, true}}, + {"amenity-doctors", {{EType::FMD_INTERNET}, true, true}}, + {"amenity-drinking_water", {{}, true, false}}, + {"amenity-embassy", {{}, true, true}}, + {"amenity-fast_food", {{EType::FMD_OPERATOR, EType::FMD_CUISINE}, true, true}}, + {"amenity-ferry_terminal", {{EType::FMD_OPERATOR}, true, true}}, + {"amenity-fire_station", {{}, true, true}}, {"amenity-fountain", {{}, true, false}}, - {"amenity-fuel", {{EType::FMD_OPERATOR, EType::FMD_OPEN_HOURS, EType::FMD_PHONE_NUMBER, EType::FMD_HEIGHT /*maxheight?*/, EType::FMD_WEBSITE}, true, true }}, + {"amenity-fuel", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, {"amenity-grave_yard", {{}, true, false}}, - {"amenity-hospital", {{EType::FMD_WEBSITE, EType::FMD_PHONE_NUMBER}, true, true}}, - {"amenity-hunting_stand", {{EType::FMD_HEIGHT}, false, false}}, - {"amenity-kindergarten", {{EType::FMD_WEBSITE, EType::FMD_PHONE_NUMBER, EType::FMD_OPERATOR, EType::FMD_WEBSITE}, true, true}}, - {"amenity-library", {{EType::FMD_PHONE_NUMBER, EType::FMD_WEBSITE, EType::FMD_FAX_NUMBER, EType::FMD_FAX_NUMBER, EType::FMD_EMAIL}, true, true}}, - {"amenity-marketplace", {{EType::FMD_OPERATOR, EType::FMD_OPEN_HOURS}, true, false}}, - {"amenity-nightclub", {{EType::FMD_WEBSITE, EType::FMD_PHONE_NUMBER, EType::FMD_OPEN_HOURS, EType::FMD_OPERATOR, EType::FMD_POSTCODE}, true, true}}, - {"amenity-parking", {{EType::FMD_OPERATOR}, true, false}}, - {"amenity-pharmacy", {{EType::FMD_OPERATOR, EType::FMD_WEBSITE}, true, true}}, - {"amenity-place_of_worship", {{EType::FMD_OPEN_HOURS, EType::FMD_WEBSITE}, true, false}}, - {"amenity-police", {{EType::FMD_OPERATOR, EType::FMD_PHONE_NUMBER, EType::FMD_WEBSITE, EType::FMD_OPEN_HOURS, EType::FMD_POSTCODE}, true, true}}, - {"amenity-post_box", {{EType::FMD_OPERATOR}, true, false}}, - {"amenity-post_office", {{EType::FMD_OPERATOR, EType::FMD_WEBSITE, EType::FMD_PHONE_NUMBER}, true, true}}, - {"amenity-pub", {{EType::FMD_OPERATOR, EType::FMD_OPEN_HOURS, EType::FMD_CUISINE, EType::FMD_PHONE_NUMBER, EType::FMD_EMAIL, EType::FMD_WEBSITE, EType::FMD_FAX_NUMBER}, true, true}}, - {"amenity-recycling", {{EType::FMD_OPERATOR, EType::FMD_WEBSITE}, true, false}}, - {"amenity-restaurant", {{EType::FMD_OPERATOR, EType::FMD_CUISINE, EType::FMD_OPEN_HOURS, EType::FMD_PHONE_NUMBER, EType::FMD_WEBSITE}, true, true}}, - {"amenity-school", {{EType::FMD_OPERATOR, EType::FMD_WIKIPEDIA}, true, true}}, - {"amenity-taxi", {{EType::FMD_OPERATOR, EType::FMD_PHONE_NUMBER}, true, false}}, + {"amenity-hospital", {{}, true, true}}, + {"amenity-hunting_stand", {{EType::FMD_HEIGHT}, true, false}}, + {"amenity-kindergarten", {{EType::FMD_OPERATOR}, true, true}}, + {"amenity-library", {{EType::FMD_INTERNET}, true, true}}, + {"amenity-marketplace", {{EType::FMD_OPERATOR}, true, true}}, + {"amenity-nightclub", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, + {"amenity-parking", {{EType::FMD_OPERATOR}, true, true}}, + {"amenity-pharmacy", {{EType::FMD_OPERATOR}, true, true}}, + {"amenity-place_of_worship", {{}, true, true}}, + {"amenity-police", {{}, true, true}}, + {"amenity-post_box", {{EType::FMD_OPERATOR, EType::FMD_POSTCODE}, true, false}}, + {"amenity-post_office", {{EType::FMD_OPERATOR, EType::FMD_POSTCODE, EType::FMD_INTERNET}, true, true}}, + {"amenity-pub", {{EType::FMD_OPERATOR, EType::FMD_CUISINE, EType::FMD_INTERNET}, true, true}}, + {"amenity-recycling", {{EType::FMD_OPERATOR}, true, false}}, + {"amenity-restaurant", {{EType::FMD_OPERATOR, EType::FMD_CUISINE, EType::FMD_INTERNET}, true, true}}, + {"amenity-school", {{EType::FMD_OPERATOR}, true, true}}, + {"amenity-taxi", {{EType::FMD_OPERATOR}, true, false}}, {"amenity-telephone", {{EType::FMD_OPERATOR, EType::FMD_PHONE_NUMBER}, false, false}}, - {"amenity-theatre", {{EType::FMD_OPERATOR, EType::FMD_WEBSITE, EType::FMD_PHONE_NUMBER, EType::FMD_POSTCODE}, true, true}}, - {"amenity-toilets", {{EType::FMD_OPEN_HOURS, EType::FMD_OPERATOR}, true, false}}, - {"amenity-townhall", {{EType::FMD_OPERATOR}, true, true}}, - {"amenity-university", {{EType::FMD_OPERATOR, EType::FMD_PHONE_NUMBER, EType::FMD_WEBSITE, EType::FMD_FAX_NUMBER, EType::FMD_EMAIL, }, true, true}}, + {"amenity-theatre", {{}, true, true}}, + {"amenity-toilets", {{EType::FMD_OPERATOR, EType::FMD_OPEN_HOURS}, true, false}}, + {"amenity-townhall", {{}, true, true}}, + {"amenity-university", {{}, true, true}}, {"amenity-waste_disposal", {{EType::FMD_OPERATOR, EType::FMD_WEBSITE}, false, false}}, {"highway-bus_stop", {{EType::FMD_OPERATOR}, true, false}}, - {"historic-archaeological_site", {{}, true, false}}, + {"historic-archaeological_site", {{EType::FMD_WIKIPEDIA}, true, false}}, {"historic-castle", {{EType::FMD_WIKIPEDIA}, true, false}}, - {"historic-memorial", {{}, true, false}}, - {"historic-monument", {{}, true, false}}, - {"historic-ruins", {{}, true, false}}, - {"internet-access", {{EType::FMD_INTERNET /*??*/}, false, false}}, - {"internet-access|wlan", {{EType::FMD_INTERNET /*??*/}, false, false}}, - {"landuse-cemetery", {{}, true, false}}, - {"leisure-garden", {{}, true, false}}, - {"leisure-sports_centre", {{}, true, true}}, - {"leisure-stadium", {{EType::FMD_WIKIPEDIA, EType::FMD_WEBSITE, EType::FMD_OPERATOR}, true, true}}, - {"leisure-swimming_pool", {{EType::FMD_OPEN_HOURS, EType::FMD_OPERATOR}, true, false}}, + {"historic-memorial", {{EType::FMD_WIKIPEDIA}, true, false}}, + {"historic-monument", {{EType::FMD_WIKIPEDIA}, true, false}}, + {"historic-ruins", {{EType::FMD_WIKIPEDIA}, true, false}}, + {"internet-access", {{EType::FMD_INTERNET}, false, false}}, + {"internet-access|wlan", {{EType::FMD_INTERNET}, false, false}}, + {"landuse-cemetery", {{EType::FMD_WIKIPEDIA}, true, false}}, + {"leisure-garden", {{EType::FMD_OPEN_HOURS, EType::FMD_INTERNET}, true, false}}, + {"leisure-sports_centre", {{EType::FMD_INTERNET}, true, true}}, + {"leisure-stadium", {{EType::FMD_WIKIPEDIA, EType::FMD_OPERATOR}, true, true}}, + {"leisure-swimming_pool", {{EType::FMD_OPERATOR}, true, true}}, {"natural-peak", {{EType::FMD_WIKIPEDIA, EType::FMD_ELE}, true, false}}, - {"natural-spring", {{}, true, false}}, - {"natural-waterfall", {{}, true, false}}, - {"office-company", {{}, true, false}}, - {"office-government", {{}, true, false}}, - {"office-lawyer", {{EType::FMD_OPEN_HOURS, EType::FMD_PHONE_NUMBER, EType::FMD_FAX_NUMBER, EType::FMD_WEBSITE, EType::FMD_EMAIL}, true, false}}, - {"office-telecommunication", {{EType::FMD_OPEN_HOURS, EType::FMD_OPERATOR}, true, false}}, + {"natural-spring", {{EType::FMD_WIKIPEDIA}, true, false}}, + {"natural-waterfall", {{EType::FMD_WIKIPEDIA}, true, false}}, + {"office-company", {{}, true, true}}, + {"office-government", {{}, true, true}}, + {"office-lawyer", {{}, true, true}}, + {"office-telecommunication", {{EType::FMD_INTERNET, EType::FMD_OPERATOR}, true, true}}, {"place-farm", {{EType::FMD_WIKIPEDIA}, true, false}}, {"place-hamlet", {{EType::FMD_WIKIPEDIA}, true, false}}, {"place-village", {{EType::FMD_WIKIPEDIA}, true, false}}, @@ -133,50 +147,50 @@ static unordered_map<string, TypeDescription> const gEditableTypes = { {"railway-station", {{EType::FMD_OPERATOR}, true, false}}, {"railway-subway_entrance", {{}, true, false}}, {"railway-tram_stop", {{EType::FMD_OPERATOR}, true, false}}, - {"shop-alcohol", {{EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-bakery", {{EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-beauty", {{EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-beverages", {{EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-bicycle", {{EType::FMD_OPERATOR, EType::FMD_WEBSITE, EType::FMD_OPEN_HOURS, EType::FMD_POSTCODE}, true, true}}, - {"shop-books", {{EType::FMD_OPEN_HOURS, EType::FMD_OPERATOR}, true, false}}, - {"shop-butcher", {{EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-car", {{EType::FMD_OPERATOR, EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-car_repair", {{EType::FMD_OPERATOR, EType::FMD_WEBSITE, EType::FMD_PHONE_NUMBER, EType::FMD_POSTCODE}, true, true}}, - {"shop-chemist", {{EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-clothes", {{EType::FMD_OPERATOR, EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-computer", {{EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-confectionery", {{EType::FMD_OPEN_HOURS}, true, false }}, - {"shop-convenience", {{EType::FMD_OPERATOR, EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-department_store", {{EType::FMD_OPERATOR, EType::FMD_OPEN_HOURS}, false, false}}, - {"shop-doityourself", {{EType::FMD_OPERATOR, EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-electronics", {{EType::FMD_OPEN_HOURS, EType::FMD_OPERATOR}, true, false}}, - {"shop-florist", {{EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-furniture", {{EType::FMD_OPEN_HOURS}, false, false}}, - {"shop-garden_centre", {{EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-gift", {{EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-greengrocer", {{EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-hairdresser", {{EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-hardware", {{EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-jewelry", {{EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-kiosk", {{EType::FMD_OPERATOR, EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-laundry", {{EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-mall", {{EType::FMD_OPERATOR, EType::FMD_OPEN_HOURS}, true, true}}, - {"shop-mobile_phone", {{EType::FMD_OPERATOR, EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-optician", {{EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-shoes", {{EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-sports", {{EType::FMD_OPEN_HOURS}, true, false}}, - {"shop-supermarket", {{EType::FMD_OPEN_HOURS, EType::FMD_OPERATOR}, true, false}}, - {"shop-toys", {{EType::FMD_OPEN_HOURS}, true, false}}, - {"tourism-alpine_hut", {{EType::FMD_ELE, EType::FMD_OPEN_HOURS, EType::FMD_OPERATOR}, true, false}}, + {"shop-alcohol", {{EType::FMD_INTERNET}, true, true}}, + {"shop-bakery", {{EType::FMD_INTERNET}, true, true}}, + {"shop-beauty", {{EType::FMD_INTERNET}, true, true}}, + {"shop-beverages", {{EType::FMD_INTERNET}, true, true}}, + {"shop-bicycle", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, + {"shop-books", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, + {"shop-butcher", {{EType::FMD_INTERNET}, true, true}}, + {"shop-car", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, + {"shop-car_repair", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, + {"shop-chemist", {{EType::FMD_INTERNET}, true, true}}, + {"shop-clothes", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, + {"shop-computer", {{EType::FMD_INTERNET}, true, true}}, + {"shop-confectionery", {{EType::FMD_INTERNET}, true, true }}, + {"shop-convenience", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, + {"shop-department_store", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, false, true}}, + {"shop-doityourself", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, + {"shop-electronics", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, + {"shop-florist", {{EType::FMD_INTERNET}, true, true}}, + {"shop-furniture", {{EType::FMD_INTERNET}, false, true}}, + {"shop-garden_centre", {{EType::FMD_INTERNET}, true, true}}, + {"shop-gift", {{EType::FMD_INTERNET}, true, true}}, + {"shop-greengrocer", {{EType::FMD_INTERNET}, true, true}}, + {"shop-hairdresser", {{EType::FMD_INTERNET}, true, true}}, + {"shop-hardware", {{EType::FMD_INTERNET}, true, true}}, + {"shop-jewelry", {{EType::FMD_INTERNET}, true, true}}, + {"shop-kiosk", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, + {"shop-laundry", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, + {"shop-mall", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, + {"shop-mobile_phone", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, + {"shop-optician", {{EType::FMD_INTERNET}, true, true}}, + {"shop-shoes", {{EType::FMD_INTERNET}, true, true}}, + {"shop-sports", {{EType::FMD_INTERNET}, true, true}}, + {"shop-supermarket", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, + {"shop-toys", {{EType::FMD_INTERNET}, true, true}}, + {"tourism-alpine_hut", {{EType::FMD_ELE, EType::FMD_OPEN_HOURS, EType::FMD_OPERATOR, EType::FMD_WEBSITE, EType::FMD_INTERNET}, true, false}}, {"tourism-artwork", {{EType::FMD_WEBSITE, EType::FMD_WIKIPEDIA}, true, false}}, - {"tourism-camp_site", {{EType::FMD_OPERATOR, EType::FMD_WEBSITE, EType::FMD_OPEN_HOURS}, true, false}}, - {"tourism-caravan_site", {{EType::FMD_WEBSITE, EType::FMD_OPERATOR}, true, false}}, - {"tourism-guest_house", {{EType::FMD_OPERATOR, EType::FMD_WEBSITE}, true, false}}, - {"tourism-hostel", {{EType::FMD_OPERATOR, EType::FMD_WEBSITE}, true, true}}, - {"tourism-hotel", {{EType::FMD_OPERATOR, EType::FMD_WEBSITE, EType::FMD_PHONE_NUMBER}, true, true}}, + {"tourism-camp_site", {{EType::FMD_OPERATOR, EType::FMD_WEBSITE, EType::FMD_OPEN_HOURS, EType::FMD_INTERNET}, true, false}}, + {"tourism-caravan_site", {{EType::FMD_WEBSITE, EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, false}}, + {"tourism-guest_house", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, + {"tourism-hostel", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, + {"tourism-hotel", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, {"tourism-information", {{}, true, false}}, - {"tourism-motel", {{EType::FMD_OPERATOR}, true, true}}, - {"tourism-museum", {{EType::FMD_OPERATOR, EType::FMD_OPEN_HOURS}, true, false}}, + {"tourism-motel", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, + {"tourism-museum", {{EType::FMD_OPERATOR, EType::FMD_INTERNET}, true, true}}, {"tourism-viewpoint", {{}, true, false}}, {"waterway-waterfall", {{EType::FMD_HEIGHT}, true, false}}}; @@ -189,29 +203,14 @@ TypeDescription const * GetTypeDescription(uint32_t const type) return nullptr; } -template <typename TIterator> -TEditableFields GetEditableFields(TIterator from, TIterator const to) +uint32_t MigrateFeatureIndex(XMLFeature const & /*xml*/) { - TEditableFields fields; - while (from != to) - { - auto const * desc = GetTypeDescription(*from++); - if (desc) - { - for (auto field : desc->fields) - fields.insert(field); - } - } - - return fields; + // @TODO(mgsergio): Update feature's index when user has downloaded fresh MWM file and old indices point to other features. + // Possible implementation: use function to load features in rect (center feature's point) and somehow compare/choose from them. + // Probably we need to store more data about features in xml, e.g. types, may be other data, to match them correctly. + return 0; } -Editor::TTypes GetAllTypes(FeatureType const & feature) -{ - Editor::TTypes types; - feature.ForEachType([&types](uint32_t type) { types.push_back(type); }); - return types; -} } // namespace Editor & Editor::Instance() @@ -255,7 +254,7 @@ void Editor::LoadMapEdits() MwmSet::MwmId const id = m_mwmIdByMapNameFn(mapName); if (!id.IsAlive()) { - // TODO(AlexZ): Handle case when map was upgraded and edits should migrate to fresh map data. + // TODO(AlexZ): MWM file was deleted, but changes remain. What should we do in this case? LOG(LWARNING, (mapName, "version", mapVersion, "references not existing MWM file.")); continue; } @@ -267,23 +266,23 @@ void Editor::LoadMapEdits() try { XMLFeature const xml(nodeOrWay.node()); - FeatureID const fid(id, xml.GetOffset()); - FeatureTypeInfo fti; - - /// TODO(mgsergio): uncomment when feature creating will - /// be required - // if (xml.GetType() != XMLFeature::Type::Way) - // { - // TODO(mgsergio): Check if feature can be read. - fti.m_feature = *m_featureLoaderFn(fid); - fti.m_feature.ApplyPatch(xml); - // } - // else - // { - // fti.m_feature = FeatureType::FromXML(xml); - // } + uint32_t const featureIndex = mapVersion == id.GetInfo()->GetVersion() ? xml.GetMWMFeatureIndex() : MigrateFeatureIndex(xml); + FeatureID const fid(id, featureIndex); + + FeatureTypeInfo & fti = m_features[id][fid.m_index]; + + if (section.first == FeatureStatus::Created) + { + // TODO(mgsergio): Create features which are not present in mwm. + } + else + { + fti.m_feature = *m_featureLoaderFn(fid); + fti.m_feature.ApplyPatch(xml); + } fti.m_feature.SetID(fid); + fti.m_street = xml.GetTagValue(kAddrStreetTag); fti.m_modificationTimestamp = xml.GetModificationTime(); ASSERT_NOT_EQUAL(my::INVALID_TIME_STAMP, fti.m_modificationTimestamp, ()); @@ -291,11 +290,13 @@ void Editor::LoadMapEdits() fti.m_uploadStatus = xml.GetUploadStatus(); fti.m_uploadError = xml.GetUploadError(); fti.m_status = section.first; - - /// Call to m_featureLoaderFn indirectly tries to load feature by - /// it's ID from the editor's m_features. - /// That's why insertion into m_features should go AFTER call to m_featureLoaderFn. - m_features[id][fid.m_index] = fti; + switch (section.first) + { + case FeatureStatus::Deleted: ++deleted; break; + case FeatureStatus::Modified: ++modified; break; + case FeatureStatus::Created: ++created; break; + case FeatureStatus::Untouched: ASSERT(false, ()); break; + } } catch (editor::XMLFeatureError const & ex) { @@ -328,11 +329,13 @@ void Editor::Save(string const & fullFilePath) const xml_node deleted = mwmNode.append_child(kDeleteSection); xml_node modified = mwmNode.append_child(kModifySection); xml_node created = mwmNode.append_child(kCreateSection); - for (auto const & offset : mwm.second) + for (auto const & index : mwm.second) { - FeatureTypeInfo const & fti = offset.second; + FeatureTypeInfo const & fti = index.second; XMLFeature xf = fti.m_feature.ToXML(); - xf.SetOffset(offset.first); + xf.SetMWMFeatureIndex(index.first); + if (!fti.m_street.empty()) + xf.SetTagValue(kAddrStreetTag, fti.m_street); ASSERT_NOT_EQUAL(0, fti.m_modificationTimestamp, ()); xf.SetModificationTime(fti.m_modificationTimestamp); if (fti.m_uploadAttemptTimestamp != my::INVALID_TIME_STAMP) @@ -363,7 +366,7 @@ void Editor::Save(string const & fullFilePath) const } } -Editor::FeatureStatus Editor::GetFeatureStatus(MwmSet::MwmId const & mwmId, uint32_t offset) const +Editor::FeatureStatus Editor::GetFeatureStatus(MwmSet::MwmId const & mwmId, uint32_t index) const { // Most popular case optimization. if (m_features.empty()) @@ -373,11 +376,11 @@ Editor::FeatureStatus Editor::GetFeatureStatus(MwmSet::MwmId const & mwmId, uint if (mwmMatched == m_features.end()) return FeatureStatus::Untouched; - auto const offsetMatched = mwmMatched->second.find(offset); - if (offsetMatched == mwmMatched->second.end()) + auto const matchedIndex = mwmMatched->second.find(index); + if (matchedIndex == mwmMatched->second.end()) return FeatureStatus::Untouched; - return offsetMatched->second.m_status; + return matchedIndex->second.m_status; } void Editor::DeleteFeature(FeatureType const & feature) @@ -401,20 +404,26 @@ void Editor::DeleteFeature(FeatureType const & feature) //FeatureID GenerateNewFeatureId(FeatureID const & oldFeatureId) //{ // // TODO(AlexZ): Stable & unique features ID generation. -// static uint32_t newOffset = 0x0effffff; -// return FeatureID(oldFeatureId.m_mwmId, newOffset++); +// static uint32_t newIndex = 0x0effffff; +// return FeatureID(oldFeatureId.m_mwmId, newIndex++); //} //} // namespace -void Editor::EditFeature(FeatureType & editedFeature) +void Editor::EditFeature(FeatureType const & editedFeature, string const & editedStreet, + string const & editedHouseNumber) { // TODO(AlexZ): Check if feature has not changed and reset status. FeatureID const fid = editedFeature.GetID(); - FeatureTypeInfo & ftInfo = m_features[fid.m_mwmId][fid.m_index]; - ftInfo.m_status = FeatureStatus::Modified; - ftInfo.m_feature = editedFeature; + FeatureTypeInfo & fti = m_features[fid.m_mwmId][fid.m_index]; + fti.m_status = FeatureStatus::Modified; + fti.m_feature = editedFeature; // TODO: What if local client time is absolutely wrong? - ftInfo.m_modificationTimestamp = time(nullptr); + fti.m_modificationTimestamp = time(nullptr); + + fti.m_street = editedStreet; + if (editedHouseNumber.empty() || feature::IsHouseNumber(editedHouseNumber)) + fti.m_feature.SetHouseNumber(editedHouseNumber); + // TODO(AlexZ): Store edited house number as house name if feature::IsHouseNumber() returned false. // TODO(AlexZ): Synchronize Save call/make it on a separate thread. Save(GetEditorFilePath()); @@ -434,12 +443,12 @@ void Editor::ForEachFeatureInMwmRectAndScale(MwmSet::MwmId const & id, // TODO(AlexZ): Check that features are visible at this scale. // Process only new (created) features. - for (auto const & offset : mwmFound->second) + for (auto const & index : mwmFound->second) { - FeatureTypeInfo const & ftInfo = offset.second; + FeatureTypeInfo const & ftInfo = index.second; if (ftInfo.m_status == FeatureStatus::Created && rect.IsPointInside(ftInfo.m_feature.GetCenter())) - f(FeatureID(id, offset.first)); + f(FeatureID(id, index.first)); } } @@ -454,60 +463,160 @@ void Editor::ForEachFeatureInMwmRectAndScale(MwmSet::MwmId const & id, // TODO(AlexZ): Check that features are visible at this scale. // Process only new (created) features. - for (auto & offset : mwmFound->second) + for (auto & index : mwmFound->second) { - FeatureTypeInfo & ftInfo = offset.second; + FeatureTypeInfo & ftInfo = index.second; if (ftInfo.m_status == FeatureStatus::Created && rect.IsPointInside(ftInfo.m_feature.GetCenter())) f(ftInfo.m_feature); } } -bool Editor::GetEditedFeature(MwmSet::MwmId const & mwmId, uint32_t offset, FeatureType & outFeature) const +bool Editor::GetEditedFeature(MwmSet::MwmId const & mwmId, uint32_t index, FeatureType & outFeature) const { auto const mwmMatched = m_features.find(mwmId); if (mwmMatched == m_features.end()) return false; - auto const offsetMatched = mwmMatched->second.find(offset); - if (offsetMatched == mwmMatched->second.end()) + auto const matchedIndex = mwmMatched->second.find(index); + if (matchedIndex == mwmMatched->second.end()) return false; // TODO(AlexZ): Should we process deleted/created features as well? - outFeature = offsetMatched->second.m_feature; + outFeature = matchedIndex->second.m_feature; return true; } vector<Metadata::EType> Editor::EditableMetadataForType(FeatureType const & feature) const { // TODO(mgsergio): Load editable fields into memory from XML and query them here. - auto const types = GetAllTypes(feature); - - auto const fields = GetEditableFields(begin(types), end(types)); + feature::TypesHolder const types(feature); + set<Metadata::EType> fields; + auto const & isBuilding = ftypes::IsBuildingChecker::Instance(); + for (auto type : types) + { + auto const * desc = GetTypeDescription(type); + if (desc) + { + for (auto field : desc->fields) + fields.insert(field); + // If address is editable, many metadata fields are editable too. + if (desc->address) + { + fields.insert(EType::FMD_EMAIL); + fields.insert(EType::FMD_OPEN_HOURS); + fields.insert(EType::FMD_PHONE_NUMBER); + fields.insert(EType::FMD_WEBSITE); + } + } + else if (isBuilding.HasTypeValue(type)) + { + // Post boxes and post offices have editable postcode field defined separately. + fields.insert(EType::FMD_POSTCODE); + } + } return {begin(fields), end(fields)}; } bool Editor::IsNameEditable(FeatureType const & feature) const { - for (auto type : GetAllTypes(feature)) + feature::TypesHolder const types(feature); + for (auto type : types) { auto const * typeDesc = GetTypeDescription(type); if (typeDesc && typeDesc->name) return true; } - return false; } bool Editor::IsAddressEditable(FeatureType const & feature) const { - for (auto type : GetAllTypes(feature)) + feature::TypesHolder const types(feature); + auto & isBuilding = ftypes::IsBuildingChecker::Instance(); + for (auto type : types) { + // Building addresses are always editable. + if (isBuilding.HasTypeValue(type)) + return true; auto const * typeDesc = GetTypeDescription(type); if (typeDesc && typeDesc->address) return true; } - return false; } + +void Editor::UploadChanges(string const & key, string const & secret, TChangesetTags const & tags) +{ + // TODO(AlexZ): features access should be synchronized. + auto const lambda = [this](string key, string secret, TChangesetTags tags) + { + int uploadedFeaturesCount = 0; + // TODO(AlexZ): insert usefull changeset comments. + ChangesetWrapper changeset({key, secret}, tags); + for (auto & id : m_features) + { + for (auto & index : id.second) + { + FeatureTypeInfo & fti = index.second; + // Do not process already uploaded features or those failed permanently. + if (!(fti.m_uploadStatus.empty() || fti.m_uploadStatus == kNeedsRetry)) + continue; + + // TODO(AlexZ): Create/delete nodes support. + if (fti.m_status != FeatureStatus::Modified) + continue; + + XMLFeature feature = fti.m_feature.ToXML(); + // TODO(AlexZ): Add areas(ways) upload support. + if (feature.GetType() != XMLFeature::Type::Node) + continue; + + try + { + XMLFeature osmFeature = changeset.GetMatchingFeatureFromOSM(feature, fti.m_feature); + XMLFeature const osmFeatureCopy = osmFeature; + osmFeature.ApplyPatch(feature); + // Check to avoid duplicates. + if (osmFeature == osmFeatureCopy) + { + LOG(LWARNING, ("Local changes are equal to OSM, feature was not uploaded, local changes were deleted.", feature)); + // TODO(AlexZ): Delete local change. + continue; + } + LOG(LDEBUG, ("Uploading patched feature", osmFeature)); + changeset.ModifyNode(osmFeature); + fti.m_uploadStatus = kUploaded; + fti.m_uploadAttemptTimestamp = time(nullptr); + ++uploadedFeaturesCount; + } + catch (ChangesetWrapper::OsmObjectWasDeletedException const & ex) + { + fti.m_uploadStatus = kDeletedFromOSMServer; + fti.m_uploadAttemptTimestamp = time(nullptr); + fti.m_uploadError = "Node was deleted from the server."; + LOG(LWARNING, (fti.m_uploadError, ex.what())); + } + catch (RootException const & ex) + { + LOG(LWARNING, (ex.what())); + fti.m_uploadStatus = kNeedsRetry; + fti.m_uploadAttemptTimestamp = time(nullptr); + fti.m_uploadError = ex.what(); + } + // TODO(AlexZ): Synchronize save after edits. + // Call Save every time we modify each feature's information. + Save(GetEditorFilePath()); + } + } + // TODO(AlexZ): Should we call any callback at the end? + }; + + // Do not run more than one upload thread at a time. + static auto future = async(launch::async, lambda, key, secret, tags); + auto const status = future.wait_for(milliseconds(0)); + if (status == future_status::ready) + future = async(launch::async, lambda, key, secret, tags); +} + } // namespace osm diff --git a/indexer/osm_editor.hpp b/indexer/osm_editor.hpp index c522900b7e..671a7ce987 100644 --- a/indexer/osm_editor.hpp +++ b/indexer/osm_editor.hpp @@ -38,8 +38,6 @@ public: Created }; - using TTypes = vector<uint32_t>; - static Editor & Instance(); void SetMwmIdByNameAndVersionFn(TMwmIdByMapNameFn const & fn) { m_mwmIdByMapNameFn = fn; } @@ -60,22 +58,32 @@ public: uint32_t scale); /// Easy way to check if feature was deleted, modified, created or not changed at all. - FeatureStatus GetFeatureStatus(MwmSet::MwmId const & mwmId, uint32_t offset) const; + FeatureStatus GetFeatureStatus(MwmSet::MwmId const & mwmId, uint32_t index) const; /// Marks feature as "deleted" from MwM file. void DeleteFeature(FeatureType const & feature); /// @returns false if feature wasn't edited. /// @param outFeature is valid only if true was returned. - bool GetEditedFeature(MwmSet::MwmId const & mwmId, uint32_t offset, FeatureType & outFeature) const; + bool GetEditedFeature(MwmSet::MwmId const & mwmId, uint32_t index, FeatureType & outFeature) const; /// Original feature with same FeatureID as newFeature is replaced by newFeature. - void EditFeature(FeatureType & editedFeature); + /// Please pass editedStreet only if it was changed by user. + void EditFeature(FeatureType const & editedFeature, + string const & editedStreet = "", + string const & editedHouseNumber = ""); vector<feature::Metadata::EType> EditableMetadataForType(FeatureType const & feature) const; + /// @returns true if feature's name is editable. bool IsNameEditable(FeatureType const & feature) const; + /// @returns true if street and house number are editable. bool IsAddressEditable(FeatureType const & feature) const; + using TChangesetTags = map<string, string>; + /// Tries to upload all local changes to OSM server in a separate thread. + /// @param[in] tags should provide additional information about client to use in changeset. + void UploadChanges(string const & key, string const & secret, TChangesetTags const & tags); + private: // TODO(AlexZ): Synchronize Save call/make it on a separate thread. void Save(string const & fullFilePath) const; @@ -84,9 +92,11 @@ private: { FeatureStatus m_status; FeatureType m_feature; + /// If not empty contains Feature's addr:street, edited by user. + string m_street; time_t m_modificationTimestamp = my::INVALID_TIME_STAMP; time_t m_uploadAttemptTimestamp = my::INVALID_TIME_STAMP; - /// "" | "ok" | "repeat" | "failed" + /// Is empty if upload has never occured or one of k* constants above otherwise. string m_uploadStatus; string m_uploadError; }; diff --git a/iphone/Maps/Classes/CustomViews/Login/MWMAuthorizationOSMLoginViewController.mm b/iphone/Maps/Classes/CustomViews/Login/MWMAuthorizationOSMLoginViewController.mm index 531ea2b19b..e17c589323 100644 --- a/iphone/Maps/Classes/CustomViews/Login/MWMAuthorizationOSMLoginViewController.mm +++ b/iphone/Maps/Classes/CustomViews/Login/MWMAuthorizationOSMLoginViewController.mm @@ -136,8 +136,7 @@ using namespace osm; { string const username = self.loginTextField.text.UTF8String; string const password = self.passwordTextField.text.UTF8String; - // TODO(AlexZ): Change to production. - OsmOAuth auth = OsmOAuth::IZServerAuth(); + OsmOAuth auth = OsmOAuth::ServerAuth(); OsmOAuth::AuthResult const result = auth.AuthorizePassword(username, password); dispatch_async(dispatch_get_main_queue(), ^ { diff --git a/iphone/Maps/Classes/CustomViews/Login/MWMAuthorizationWebViewLoginViewController.mm b/iphone/Maps/Classes/CustomViews/Login/MWMAuthorizationWebViewLoginViewController.mm index 8b2d1f6713..55096320cb 100644 --- a/iphone/Maps/Classes/CustomViews/Login/MWMAuthorizationWebViewLoginViewController.mm +++ b/iphone/Maps/Classes/CustomViews/Login/MWMAuthorizationWebViewLoginViewController.mm @@ -71,7 +71,7 @@ NSString * getVerifier(NSString * urlString) dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ { // TODO(AlexZ): Change to production. - OsmOAuth auth = OsmOAuth::IZServerAuth(); + OsmOAuth const auth = OsmOAuth::ServerAuth(); OsmOAuth::TUrlKeySecret urlKey; switch (self.authType) { @@ -119,7 +119,7 @@ NSString * getVerifier(NSString * urlString) { TKeySecret outKeySecret; // TODO(AlexZ): Change to production. - OsmOAuth auth = OsmOAuth::IZServerAuth(); + OsmOAuth const auth = OsmOAuth::ServerAuth(); OsmOAuth::AuthResult const result = auth.FinishAuthorization(self->m_keySecret, verifier.UTF8String, outKeySecret); dispatch_async(dispatch_get_main_queue(), ^ { diff --git a/iphone/Maps/Classes/MWMPlacePageEntity.mm b/iphone/Maps/Classes/MWMPlacePageEntity.mm index c13b556c63..4b7b5ef579 100644 --- a/iphone/Maps/Classes/MWMPlacePageEntity.mm +++ b/iphone/Maps/Classes/MWMPlacePageEntity.mm @@ -185,9 +185,10 @@ void initFieldsMap() self.title = name.length > 0 ? name : L(@"dropped_pin"); self.category = @(info.GetPinType().c_str()); - auto const presentTypes = metadata.GetPresentTypes(); + if (!info.m_house.empty()) + [self addMetaField:MWMPlacePageCellTypeBuilding value:info.m_house]; - for (auto const & type : presentTypes) + for (auto const type : metadata.GetPresentTypes()) { switch (type) { @@ -281,11 +282,19 @@ void initFieldsMap() - (void)processStreets { - // TODO Replace with real Getters - // FeatureType * feature = self.delegate.userMark->GetFeature(); - string featureString = "street#2"; - self.nearbyStreets = @[@"street#1", @(featureString.c_str()), @"street#3"]; - [self addMetaField:MWMPlacePageCellTypeStreet value:featureString]; + FeatureType const * feature = self.delegate.userMark->GetFeature(); + if (!feature) + return; + + Framework & frm = GetFramework(); + auto const streets = frm.GetNearbyFeatureStreets(*feature); + NSMutableArray * arr = [[NSMutableArray alloc] initWithCapacity:streets.size()]; + for (auto const & street : streets) + [arr addObject:@(street.c_str())]; + self.nearbyStreets = arr; + + auto const info = frm.GetFeatureAddressInfo(*feature); + [self addMetaField:MWMPlacePageCellTypeStreet value:info.m_street]; } #pragma mark - Editing @@ -295,9 +304,20 @@ void initFieldsMap() FeatureType * feature = self.delegate.userMark->GetFeature(); if (!feature) return; - vector<Metadata::EType> const editableTypes = osm::Editor::Instance().EditableMetadataForType(*feature); - if (!editableTypes.empty()) + + auto & editor = osm::Editor::Instance(); + vector<Metadata::EType> const editableTypes = editor.EditableMetadataForType(*feature); + bool const isNameEditable = editor.IsNameEditable(*feature); + bool const isAddressEditable = editor.IsAddressEditable(*feature); + if (!editableTypes.empty() || isAddressEditable || isNameEditable) [self addEditField]; + if (isNameEditable) + m_editableFields.insert(MWMPlacePageCellTypeName); + if (isAddressEditable) + { + m_editableFields.insert(MWMPlacePageCellTypeStreet); + m_editableFields.insert(MWMPlacePageCellTypeBuilding); + } for (auto const & type : editableTypes) { NSAssert(kMetaFieldsMap[type] >= Metadata::FMD_COUNT || kMetaFieldsMap[type] == 0, @"Incorrect enum value"); @@ -317,7 +337,9 @@ void initFieldsMap() NSAssert(feature != nullptr, @"Feature is null"); if (!feature) return; + auto & metadata = feature->GetMetadata(); + string streetName, houseNumber; for (auto const & cell : cells) { switch (cell.first) @@ -343,18 +365,20 @@ void initFieldsMap() } case MWMPlacePageCellTypeName: { - // TODO Add implementation + // TODO(AlexZ): Make sure that we display and save name in the same language (default?). + auto names = feature->GetNames(); + names.AddString(StringUtf8Multilang::DEFAULT_CODE, cell.second); + feature->SetNames(names); break; } case MWMPlacePageCellTypeStreet: { - // TODO Add implementation - // Save cell.second + streetName = cell.second; break; } case MWMPlacePageCellTypeBuilding: { - // TODO Add implementation + houseNumber = cell.second; break; } default: @@ -362,8 +386,7 @@ void initFieldsMap() break; } } - feature->SetMetadata(metadata); - osm::Editor::Instance().EditFeature(*feature); + osm::Editor::Instance().EditFeature(*feature, streetName, houseNumber); } #pragma mark - Getters diff --git a/iphone/Maps/Classes/MWMPlacePageViewManager.mm b/iphone/Maps/Classes/MWMPlacePageViewManager.mm index e3bdbf6591..df5ee26ea6 100644 --- a/iphone/Maps/Classes/MWMPlacePageViewManager.mm +++ b/iphone/Maps/Classes/MWMPlacePageViewManager.mm @@ -295,13 +295,17 @@ typedef NS_ENUM(NSUInteger, MWMPlacePageManagerState) Framework & f = GetFramework(); BookmarkData data = BookmarkData(self.entity.title.UTF8String, f.LastEditedBMType()); size_t const categoryIndex = f.LastEditedBMCategory(); - size_t const bookmarkIndex = f.GetBookmarkManager().AddBookmark(categoryIndex, m_userMark->GetUserMark()->GetPivot(), data); + m2::PointD const mercator = m_userMark->GetUserMark()->GetPivot(); + size_t const bookmarkIndex = f.GetBookmarkManager().AddBookmark(categoryIndex, mercator, data); self.entity.bac = make_pair(categoryIndex, bookmarkIndex); self.entity.type = MWMPlacePageEntityTypeBookmark; BookmarkCategory::Guard guard(*f.GetBmCategory(categoryIndex)); UserMark const * bookmark = guard.m_controller.GetUserMark(bookmarkIndex); + // TODO(AlexZ): Refactor bookmarks code together to hide this code in the Framework/Drape. + // UI code should never know about any guards, pointers to UserMark etc. + const_cast<UserMark *>(bookmark)->SetFeature(f.GetFeatureAtMercatorPoint(mercator)); m_userMark.reset(new UserMarkCopy(bookmark, false)); [NSNotificationCenter.defaultCenter postNotificationName:kBookmarksChangedNotification object:nil @@ -324,6 +328,8 @@ typedef NS_ENUM(NSUInteger, MWMPlacePageManagerState) self.entity.type = MWMPlacePageEntityTypeRegular; + // TODO(AlexZ): SetFeature is called in GetAddressMark here. + // UI code should never know about any guards, pointers to UserMark etc. PoiMarkPoint const * poi = f.GetAddressMark(bookmark->GetPivot()); m_userMark.reset(new UserMarkCopy(poi, false)); if (bookmarkCategory) diff --git a/map/address_finder.cpp b/map/address_finder.cpp index a27f5fc088..353385e788 100644 --- a/map/address_finder.cpp +++ b/map/address_finder.cpp @@ -12,7 +12,7 @@ #include "platform/preferred_languages.hpp" - +/* namespace { class FeatureInfoT @@ -180,7 +180,6 @@ void Framework::GetFeatureTypes(m2::PointD const & pxPoint, vector<string> & typ getTypes.GetFeatureTypes(5, types); } -/* namespace { class DoGetAddressBase : public DoGetFeatureInfoBase @@ -479,14 +478,51 @@ search::AddressInfo Framework::GetFeatureAddressInfo(FeatureType const & ft) con //GetLocality(pt, info); info.m_house = ft.GetHouseNumber(); + // TODO(vng): Now geocoder assumes that buildings without house numbers also do not have a specified street. + if (!info.m_house.empty()) + { + // TODO(vng): Return feature's street only if it was specified in OSM data. + search::ReverseGeocoder const coder(m_model.GetIndex()); + vector<search::ReverseGeocoder::Street> const streets = coder.GetNearbyFeatureStreets(ft); + if (!streets.empty()) + info.m_street = streets.front().m_name; + } + + // TODO(vng): Why AddressInfo is responsible for types and names? + string defaultName, intName; + ft.GetPreferredNames(defaultName, intName); + info.m_name = defaultName.empty() ? intName : defaultName; + info.m_types = GetPrintableFeatureTypes(ft); - search::ReverseGeocoder const coder(m_model.GetIndex()); - vector<search::ReverseGeocoder::Street> const streets = coder.GetNearbyFeatureStreets(ft); - if (!streets.empty()) - info.m_street = streets.front().m_name; return info; } +vector<string> Framework::GetPrintableFeatureTypes(FeatureType const & ft) const +{ + ASSERT(m_searchEngine, ()); + + vector<string> results; + int8_t const locale = CategoriesHolder::MapLocaleToInteger(languages::GetCurrentOrig()); + + feature::TypesHolder types(ft); + types.SortBySpec(); + // Try to add types from categories. + for (uint32_t type : types) + { + string s; + if (m_searchEngine->GetNameByType(type, locale, s)) + results.push_back(s); + } + // If nothing added - return raw classificator types. + if (results.empty()) + { + Classificator const & c = classif(); + for (uint32_t type : types) + results.push_back(c.GetReadableObjectName(type)); + } + return results; +} + vector<string> Framework::GetNearbyFeatureStreets(FeatureType const & ft) const { search::ReverseGeocoder const coder(m_model.GetIndex()); diff --git a/map/framework.cpp b/map/framework.cpp index 910161e17b..4f7d6cb49a 100644 --- a/map/framework.cpp +++ b/map/framework.cpp @@ -322,7 +322,11 @@ Framework::Framework() editor.SetInvalidateFn([this](){ InvalidateRect(GetCurrentViewport()); }); editor.SetFeatureLoaderFn([this](FeatureID const & fid) -> unique_ptr<FeatureType> { - return GetPOIByID(fid); + unique_ptr<FeatureType> feature(new FeatureType()); + Index::FeaturesLoaderGuard const guard(m_model.GetIndex(), fid.m_mwmId); + guard.GetOriginalFeatureByIndex(fid.m_index, *feature); + feature->ParseEverything(); + return feature; }); editor.LoadMapEdits(); } @@ -1119,12 +1123,7 @@ void Framework::ShowSearchResult(search::Result const & res) { case Result::RESULT_FEATURE: { - FeatureID const id = res.GetFeatureID(); - Index::FeaturesLoaderGuard guard(m_model.GetIndex(), id.m_mwmId); - - ft.reset(new FeatureType); - guard.GetFeatureByIndex(id.m_index, *ft); - + ft = GetFeatureByID(res.GetFeatureID()); scale = GetFeatureViewportScale(TypesHolder(*ft)); center = GetCenter(*ft, scale); break; @@ -1137,6 +1136,7 @@ void Framework::ShowSearchResult(search::Result const & res) break; default: + ASSERT(false, ("Suggests should not be here.")); return; } @@ -1632,7 +1632,7 @@ unique_ptr<FeatureType> Framework::GetFeatureAtMercatorPoint(m2::PointD const & return pointFt ? move(pointFt) : move(areaFt); } -unique_ptr<FeatureType> Framework::GetPOIByID(FeatureID const & fid) const +unique_ptr<FeatureType> Framework::GetFeatureByID(FeatureID const & fid) const { ASSERT(fid.IsValid(), ()); @@ -1690,18 +1690,6 @@ public: } -void Framework::FindClosestPOIMetadata(m2::PointD const & pt, feature::Metadata & metadata) const -{ - m2::RectD rect(pt, pt); - double const inf = MercatorBounds::GetCellID2PointAbsEpsilon(); - rect.Inflate(inf, inf); - - DoFindClosestPOI doFind(pt, 1.1 /* search radius in meters */); - m_model.ForEachFeature(rect, doFind, scales::GetUpperScale() /* scale level for POI */); - - doFind.LoadMetadata(m_model, metadata); -} - BookmarkAndCategory Framework::FindBookmark(UserMark const * mark) const { BookmarkAndCategory empty = MakeEmptyBookmarkAndCategory(); @@ -1854,7 +1842,7 @@ UserMark const * Framework::OnTapEventImpl(m2::PointD pxPoint, bool isLong, bool if (fid.IsValid()) { - feature = GetPOIByID(fid); + feature = GetFeatureByID(fid); mercatorPivot = feature::GetCenter(*feature); needMark = true; } diff --git a/map/framework.hpp b/map/framework.hpp index 4172daad10..4cdb132bed 100644 --- a/map/framework.hpp +++ b/map/framework.hpp @@ -461,19 +461,16 @@ public: /// Set correct viewport, parse API, show balloon. bool ShowMapForURL(string const & url); - /// Get classificator types for nearest features. - /// @param[in] pxPoint Current touch point in device pixel coordinates. - void GetFeatureTypes(m2::PointD const & pxPoint, vector<string> & types) const; - //@} - private: // TODO(vng): Uncomment when needed. //void GetLocality(m2::PointD const & pt, search::AddressInfo & info) const; public: - /// Please use this method for debug purposes only. It always tries to find closest street. + /// @returns address of nearby building with house number in approx 1km distance. search::AddressInfo GetMercatorAddressInfo(m2::PointD const & mercator) const; + /// @returns address only if it was specified for given feature; used in the editor. search::AddressInfo GetFeatureAddressInfo(FeatureType const & ft) const; + vector<string> GetPrintableFeatureTypes(FeatureType const & ft) const; /// If feature does not have explicit street in OSM data, first value can be a closest named street. /// If it does have explicit street name in OSM, it goes first in the returned vector. /// @returns empty vector if no named streets were found around feature. @@ -483,8 +480,7 @@ public: /// @returns nullptr if no feature was found at the given mercator point. unique_ptr<FeatureType> GetFeatureAtMercatorPoint(m2::PointD const & mercator) const; // TODO(AlexZ): Do we really need to avoid linear features? - unique_ptr<FeatureType> GetPOIByID(FeatureID const & fid) const; - void FindClosestPOIMetadata(m2::PointD const & pt, feature::Metadata & metadata) const; + unique_ptr<FeatureType> GetFeatureByID(FeatureID const & fid) const; void MemoryWarning(); void EnterBackground(); diff --git a/map/map_tests/bookmarks_test.cpp b/map/map_tests/bookmarks_test.cpp index 79b813c8fa..c52c5db7f3 100644 --- a/map/map_tests/bookmarks_test.cpp +++ b/map/map_tests/bookmarks_test.cpp @@ -396,11 +396,12 @@ namespace { search::AddressInfo const info = fm.GetMercatorAddressInfo(MercatorBounds::FromLatLon(lat, lon)); - TEST_EQUAL(info.m_name, poi.m_name, ()); TEST_EQUAL(info.m_street, poi.m_street, ()); TEST_EQUAL(info.m_house, poi.m_house, ()); - TEST_EQUAL(info.m_types.size(), 1, ()); - TEST_EQUAL(info.GetBestType(), poi.m_type, ()); + // TODO(AlexZ): AddressInfo should contain addresses only. Refactor. + //TEST_EQUAL(info.m_name, poi.m_name, ()); + //TEST_EQUAL(info.m_types.size(), 1, ()); + //TEST_EQUAL(info.GetBestType(), poi.m_type, ()); } } @@ -412,30 +413,9 @@ UNIT_TEST(Bookmarks_AddressInfo) fm.RegisterMap(platform::LocalCountryFile::MakeForTesting("minsk-pass")); fm.OnSize(800, 600); - // assume that developers have English or Russian system language :) - string const lang = languages::GetCurrentNorm(); - LOG(LINFO, ("Current language =", lang)); - - // default name (street in russian, category in english). - size_t index = 0; - if (lang == "ru") - index = 1; - if (lang == "en") - index = 2; - - POIInfo poi1[] = { - { "Планета Pizza", "улица Карла Маркса", "10", "cafe" }, - { "Планета Pizza", "улица Карла Маркса", "10", "кафе" }, - { "Планета Pizza", "vulica Karla Marksa", "10", "cafe" } - }; - CheckPlace(fm, 53.8964918, 27.555559, poi1[index]); - - POIInfo poi2[] = { - { "Нц Шашек И Шахмат", "улица Карла Маркса", "10", "hotel" }, - { "Нц Шашек И Шахмат", "улица Карла Маркса", "10", "гостиница" }, - { "Нц Шашек И Шахмат", "vulica Karla Marksa", "10", "hotel" } - }; - CheckPlace(fm, 53.8964365, 27.5554007, poi2[index]); + // Our code always uses "default" street name for addresses. + CheckPlace(fm, 53.8964918, 27.555559, { "Планета Pizza", "улица Карла Маркса", "10", "cafe" }); + CheckPlace(fm, 53.8964365, 27.5554007, { "Нц Шашек И Шахмат", "улица Карла Маркса", "10", "hotel" }); } UNIT_TEST(Bookmarks_IllegalFileName) diff --git a/qt/draw_widget.cpp b/qt/draw_widget.cpp index c75f112699..f17c913519 100644 --- a/qt/draw_widget.cpp +++ b/qt/draw_widget.cpp @@ -470,15 +470,14 @@ void DrawWidget::ShowPOIEditor(FeatureType & feature) { // Show Edit POI dialog. auto & editor = osm::Editor::Instance(); - EditorDialog dlg(this, feature); + EditorDialog dlg(this, feature, *m_framework); int const result = dlg.exec(); if (result == QDialog::Accepted) { - // Save edited data. feature.SetNames(dlg.GetEditedNames()); - // Save edited metadata. feature.SetMetadata(dlg.GetEditedMetadata()); - editor.EditFeature(feature); + // TODO(AlexZ): Check that street was actually changed/edited. + editor.EditFeature(feature, dlg.GetEditedStreet(), dlg.GetEditedHouseNumber()); } else if (result == QDialogButtonBox::DestructiveRole) { @@ -496,20 +495,18 @@ void DrawWidget::ShowInfoPopup(QMouseEvent * e, m2::PointD const & pt) }; search::AddressInfo const info = m_framework->GetMercatorAddressInfo(m_framework->PtoG(pt)); - - // Get feature types under cursor. - vector<string> types; - m_framework->GetFeatureTypes(pt, types); - for (size_t i = 0; i < types.size(); ++i) - addStringFn(types[i]); + for (auto const & type : info.m_types) + addStringFn(type); menu.addSeparator(); - // Format address and types. if (!info.m_name.empty()) + { addStringFn(info.m_name); + menu.addSeparator(); + } + addStringFn(info.FormatAddress()); - addStringFn(info.FormatTypes()); menu.exec(e->pos()); } diff --git a/qt/editor_dialog.cpp b/qt/editor_dialog.cpp index 359418be95..d0b460bdb6 100644 --- a/qt/editor_dialog.cpp +++ b/qt/editor_dialog.cpp @@ -1,9 +1,12 @@ #include "qt/editor_dialog.hpp" +#include "map/framework.hpp" + #include "search/result.hpp" #include "indexer/classificator.hpp" #include "indexer/feature.hpp" +#include "indexer/feature_algo.hpp" #include "indexer/osm_editor.hpp" #include "base/collection_cast.hpp" @@ -11,6 +14,7 @@ #include "std/set.hpp" #include "std/vector.hpp" +#include <QtWidgets/QComboBox> #include <QtWidgets/QDialogButtonBox> #include <QtWidgets/QHBoxLayout> #include <QtWidgets/QLabel> @@ -22,10 +26,25 @@ using feature::Metadata; -EditorDialog::EditorDialog(QWidget * parent, FeatureType const & feature) : QDialog(parent) +constexpr char const * kStreetObjectName = "addr:street"; +constexpr char const * kHouseNumberObjectName = "addr:housenumber"; + +EditorDialog::EditorDialog(QWidget * parent, FeatureType const & feature, Framework & frm) : QDialog(parent) { + osm::Editor & editor = osm::Editor::Instance(); + QVBoxLayout * vLayout = new QVBoxLayout(); + // Zero uneditable row: coordinates. + ms::LatLon const ll = MercatorBounds::ToLatLon(feature::GetCenter(feature)); + QHBoxLayout * coordinatesRow = new QHBoxLayout(); + coordinatesRow->addWidget(new QLabel("Latitude, Longitude:")); + QLabel * coords = new QLabel(QString::fromStdString(strings::to_string_dac(ll.lat, 6) + + "," + strings::to_string_dac(ll.lon, 6))); + coords->setTextInteractionFlags(Qt::TextSelectableByMouse); + coordinatesRow->addWidget(coords); + vLayout->addLayout(coordinatesRow); + // First uneditable row: feature types. string strTypes; feature.ForEachType([&strTypes](uint32_t type) @@ -37,48 +56,70 @@ EditorDialog::EditorDialog(QWidget * parent, FeatureType const & feature) : QDia typesRow->addWidget(new QLabel(QString::fromStdString(strTypes))); vLayout->addLayout(typesRow); - if (osm::Editor::Instance().IsNameEditable(feature)) + bool const readOnlyName = !editor.IsNameEditable(feature); + // Rows block: Name(s) label(s) and text input. + char const * defaultLangStr = StringUtf8Multilang::GetLangByCode(StringUtf8Multilang::DEFAULT_CODE); + // Default name editor is always displayed, even if feature name is empty. + QHBoxLayout * defaultNameRow = new QHBoxLayout(); + defaultNameRow->addWidget(new QLabel(QString("name"))); + QLineEdit * defaultNamelineEdit = new QLineEdit(); + defaultNamelineEdit->setReadOnly(readOnlyName); + defaultNamelineEdit->setObjectName(defaultLangStr); + defaultNameRow->addWidget(defaultNamelineEdit); + vLayout->addLayout(defaultNameRow); + + feature.ForEachNameRef([&](int8_t langCode, string const & name) -> bool { - // Rows block: Name(s) label(s) and text input. - char const * defaultLangStr = StringUtf8Multilang::GetLangByCode(StringUtf8Multilang::DEFAULT_CODE); - // Default name editor is always displayed, even if feature name is empty. - QHBoxLayout * defaultNameRow = new QHBoxLayout(); - defaultNameRow->addWidget(new QLabel(QString("Name:"))); - QLineEdit * defaultNamelineEdit = new QLineEdit(); - defaultNamelineEdit->setObjectName(defaultLangStr); - defaultNameRow->addWidget(defaultNamelineEdit); - vLayout->addLayout(defaultNameRow); - - feature.ForEachNameRef([&vLayout, &defaultNamelineEdit](int8_t langCode, string const & name) -> bool + if (langCode == StringUtf8Multilang::DEFAULT_CODE) + defaultNamelineEdit->setText(QString::fromStdString(name)); + else { - if (langCode == StringUtf8Multilang::DEFAULT_CODE) - defaultNamelineEdit->setText(QString::fromStdString(name)); - else - { - QHBoxLayout * nameRow = new QHBoxLayout(); - char const * langStr = StringUtf8Multilang::GetLangByCode(langCode); - nameRow->addWidget(new QLabel(QString("Name:") + langStr)); - QLineEdit * lineEditName = new QLineEdit(QString::fromStdString(name)); - lineEditName->setObjectName(langStr); - nameRow->addWidget(lineEditName); - vLayout->addLayout(nameRow); - } - return true; // true is needed to enumerate all languages. - }); - } + QHBoxLayout * nameRow = new QHBoxLayout(); + char const * langStr = StringUtf8Multilang::GetLangByCode(langCode); + nameRow->addWidget(new QLabel(QString("name:") + langStr)); + QLineEdit * lineEditName = new QLineEdit(QString::fromStdString(name)); + lineEditName->setReadOnly(readOnlyName); + lineEditName->setObjectName(langStr); + nameRow->addWidget(lineEditName); + vLayout->addLayout(nameRow); + } + return true; // true is needed to enumerate all languages. + }); + + // Address rows. + bool const readOnlyAddress = !editor.IsAddressEditable(feature); + vector<string> nearbyStreets = frm.GetNearbyFeatureStreets(feature); + // If feature does not have a specified street, display empty combo box. + search::AddressInfo const info = frm.GetFeatureAddressInfo(feature); + if (info.m_street.empty()) + nearbyStreets.insert(nearbyStreets.begin(), ""); + QHBoxLayout * streetRow = new QHBoxLayout(); + streetRow->addWidget(new QLabel(QString(kStreetObjectName))); + QComboBox * cmb = new QComboBox(); + for (auto const & street : nearbyStreets) + cmb->addItem(street.c_str()); + cmb->setEditable(!readOnlyAddress); + cmb->setEnabled(!readOnlyAddress); + cmb->setObjectName(kStreetObjectName); + streetRow->addWidget(cmb) ; + vLayout->addLayout(streetRow); + QHBoxLayout * houseRow = new QHBoxLayout(); + houseRow->addWidget(new QLabel(QString(kHouseNumberObjectName))); + QLineEdit * houseLineEdit = new QLineEdit(); + houseLineEdit->setText(info.m_house.c_str()); + houseLineEdit->setReadOnly(readOnlyAddress); + houseLineEdit->setObjectName(kHouseNumberObjectName); + houseRow->addWidget(houseLineEdit); + vLayout->addLayout(houseRow); // All metadata rows. QVBoxLayout * metaRows = new QVBoxLayout(); - // Features can have several types, so we merge all editable fields here. - set<Metadata::EType> const editableMetadataFields = - // TODO(mgsergio, Alex): Maybe just return set in EditableMetadataForType? - my::collection_cast<set>(osm::Editor::Instance().EditableMetadataForType(feature)); - + vector<Metadata::EType> const editableMetadataFields = editor.EditableMetadataForType(feature); for (Metadata::EType const field : editableMetadataFields) { QString const fieldName = QString::fromStdString(DebugPrint(field)); QHBoxLayout * fieldRow = new QHBoxLayout(); - fieldRow->addWidget(new QLabel(fieldName + ":")); + fieldRow->addWidget(new QLabel(fieldName)); QLineEdit * lineEdit = new QLineEdit(QString::fromStdString(feature.GetMetadata().Get(field))); // Mark line editor to query it's text value when editing is finished. lineEdit->setObjectName(fieldName); @@ -133,3 +174,16 @@ Metadata EditorDialog::GetEditedMetadata() const } return metadata; } + +string EditorDialog::GetEditedStreet() const +{ + QComboBox const * cmb = findChild<QComboBox *>(); + if (cmb->count()) + return cmb->itemText(0).toStdString(); + return string(); +} + +string EditorDialog::GetEditedHouseNumber() const +{ + return findChild<QLineEdit *>(kHouseNumberObjectName)->text().toStdString(); +} diff --git a/qt/editor_dialog.hpp b/qt/editor_dialog.hpp index a383851068..964467065f 100644 --- a/qt/editor_dialog.hpp +++ b/qt/editor_dialog.hpp @@ -9,13 +9,16 @@ #include <QtWidgets/QDialog> class FeatureType; +class Framework; class QLineEdit; class EditorDialog : public QDialog { Q_OBJECT public: - EditorDialog(QWidget * parent, FeatureType const & feature); + EditorDialog(QWidget * parent, FeatureType const & feature, Framework & frm); StringUtf8Multilang GetEditedNames() const; feature::Metadata GetEditedMetadata() const; + string GetEditedStreet() const; + string GetEditedHouseNumber() const; }; diff --git a/qt/mainwindow.cpp b/qt/mainwindow.cpp index fb1c4b8f24..3a70cd2285 100644 --- a/qt/mainwindow.cpp +++ b/qt/mainwindow.cpp @@ -49,6 +49,10 @@ namespace qt { +// Defined in osm_auth_dialog.cpp. +extern char const * kTokenKeySetting; +extern char const * kTokenSecretSetting; + MainWindow::MainWindow() : m_locationService(CreateDesktopLocationService(*this)) { // Always runs on the first desktop @@ -91,6 +95,7 @@ MainWindow::MainWindow() : m_locationService(CreateDesktopLocationService(*this) helpMenu->addAction(tr("About"), this, SLOT(OnAbout())); helpMenu->addAction(tr("Preferences"), this, SLOT(OnPreferences())); helpMenu->addAction(tr("OpenStreetMap Login"), this, SLOT(OnLoginMenuItem())); + helpMenu->addAction(tr("Upload Edits"), this, SLOT(OnUploadEditsMenuItem())); #else { // create items in the system menu @@ -371,6 +376,17 @@ void MainWindow::OnLoginMenuItem() dlg.exec(); } +void MainWindow::OnUploadEditsMenuItem() +{ + string key, secret; + Settings::Get(kTokenKeySetting, key); + Settings::Get(kTokenSecretSetting, secret); + if (key.empty() || secret.empty()) + OnLoginMenuItem(); + else + osm::Editor::Instance().UploadChanges(key, secret, {{"created_by", "MAPS.ME " OMIM_OS_NAME}}); +} + void MainWindow::OnBeforeEngineCreation() { m_pDrawWidget->GetFramework().SetMyPositionModeListener([this](location::EMyPositionMode mode) diff --git a/qt/mainwindow.hpp b/qt/mainwindow.hpp index ab496bacf2..73d01ff74a 100644 --- a/qt/mainwindow.hpp +++ b/qt/mainwindow.hpp @@ -69,6 +69,7 @@ namespace qt void OnMyPosition(); void OnSearchButtonClicked(); void OnLoginMenuItem(); + void OnUploadEditsMenuItem(); void OnBeforeEngineCreation(); }; diff --git a/qt/osm_auth_dialog.cpp b/qt/osm_auth_dialog.cpp index a6bcda9e25..28ae1c08ba 100644 --- a/qt/osm_auth_dialog.cpp +++ b/qt/osm_auth_dialog.cpp @@ -102,8 +102,7 @@ void OsmAuthDialog::OnAction() return; } - // TODO(AlexZ): Change to production server. - OsmOAuth auth = osm::OsmOAuth::DevServerAuth(); + OsmOAuth auth = osm::OsmOAuth::ServerAuth(); OsmOAuth::AuthResult const res = auth.AuthorizePassword(login, password); if (res != OsmOAuth::AuthResult::OK) { |