Я пытаюсь взломать рабочий процесс R для анализа путей SVG, используя этот файл на эта веб-страница. Я сталкиваюсь с артефактами в расположении полученных полигонов:
Некоторые страны не согласуются со своими соседями - например, США / Канада, США / Мексика, Россия / азиатские соседи. Поскольку эффект затрагивает страны с более сложными полигонами, похоже, проблема связана с кумулятивным суммированием, но я не понимаю, в чем проблема в моем рабочем процессе, а именно:
nodejsмодуль svg-path-parserЗдесь я воспроизвожу полный рабочий процесс, используя R (для США / Канады), с внешним вызовом nodejs:
require(dplyr)
require(purrr)
require(stringr)
require(tidyr)
require(ggplot2)
require(rvest)
require(xml2)
require(jsonlite)
# Get and parse the SVG
doc = read_xml('https://visionscarto.net/public/fonds-de-cartes-en/visionscarto-bertin1953.svg')
countries = doc %>% html_nodes('.country')
names(countries) = html_attr(countries, 'id')
cdi = str_which(names(countries), 'CIV') # unicode in Cote d'Ivoire breaks the code
countries = countries[-cdi]
# Extract SVG paths and parse with node's svg-path-parser module.
# If you don't have node you can use this instead (note this step might be the problem):
# d = read_csv('https://gist.githubusercontent.com/geotheory/b7353a7a8a480209b31418c806cb1c9e/raw/6d3ba2a62f6e8667eef15e29a5893d9d795e8bb1/bertin_svg.csv')
d = imap_dfr(countries, ~{
message(.y)
svg_path = xml_find_all(.x, paste0("//*[@id='", .y, "']/d1:path")) %>% html_attr('d')
node_call = paste0("node -e \"var parseSVG = require('svg-path-parser'); var d='", svg_path,
"'; console.info(JSON.stringify(parseSVG(d)));\"")
system(node_call, intern = T) %>% fromJSON %>% mutate(country = .y)
}) %>% as_data_frame()
# some initial processing
d1 = d %>% filter(country %in% c('USA United States','CAN Canada')) %>%
mutate(x = replace_na(x, 0), y = replace_na(y, 0), # NAs need replacing
relative = replace_na(relative, FALSE),
grp = (command == 'closepath') %>% cumsum) # polygon grouping variable
# new object to loop through
d2 = d1 %>% mutate(x_adj = x, y_adj = y) %>% filter(command != 'closepath')
# loop through and change relative coords to absolute
for(i in 2:nrow(d2)){
if (d2$relative[i]){ # cumulative sum where coords are relative
d2$x_adj[i] = d2$x_adj[i-1] + d2$x_adj[i]
d2$y_adj[i] = d2$y_adj[i-1] + d2$y_adj[i]
} else{ # code M/L require no alteration
if (d2$code[i] == 'V') d2$x_adj[i] = d2$x_adj[i-1] # absolute vertical transform inherits previous x
if (d2$code[i] == 'H') d2$y_adj[i] = d2$y_adj[i-1] # absolute holrizontal transform etc
}
}
# plot result
d2 %>% ggplot(aes(x_adj, -y_adj, group = paste(country, grp))) +
geom_polygon(fill='white', col='black', size=.3) +
coord_equal() + guides(fill=F)
Любая помощь приветствуется. Синтаксис пути SVG указан в w3 и резюмирован более кратко здесь.
Изменить (ответ на @ccprog)
Вот данные, возвращаемые svg-path-parser для последовательности команд H:
code command x y relative country
<chr> <chr> <dbl> <dbl> <lgl> <chr>
1 l lineto -0.91 -0.6 TRUE CAN Canada
2 l lineto -0.92 -0.59 TRUE CAN Canada
3 H horizontal lineto 189. NA NA CAN Canada
4 l lineto -1.03 0.02 TRUE CAN Canada
5 l lineto -0.74 -0.07 TRUE CAN Canada
Вот как выглядит d2 для той же последовательности после цикла:
code command x y relative country grp x_adj y_adj
<chr> <chr> <dbl> <dbl> <lgl> <chr> <int> <dbl> <dbl>
1 l lineto -0.91 -0.6 TRUE CAN Canada 20 199. 143.
2 l lineto -0.92 -0.59 TRUE CAN Canada 20 198. 143.
3 H horizontal lineto 189. 0 FALSE CAN Canada 20 189. 143.
4 l lineto -1.03 0.02 TRUE CAN Canada 20 188. 143.
5 l lineto -0.74 -0.07 TRUE CAN Canada 20 187. 143.
Разве это не выглядит нормально? Когда я смотрю на необработанные значения y_adj для H и предыдущих строк, они идентичны 142.56.
d = imap_dfr(countries, ~{
message(.y)
svg_path = xml_find_all(.x, paste0("//*[@id='", .y, "']/d1:path")) %>% html_attr('d')
node_call = paste0("node -e \"var parseSVG = require('svg-path-parser'); var d='", svg_path,
"'; console.info(JSON.stringify(parseSVG.makeAbsolute(parseSVG(d))));\"")
system(node_call, intern = T) %>% fromJSON %>% mutate(country = .y)
}) %>% as_data_frame() %>%
mutate(grp = (command == 'moveto') %>% cumsum)
d %>% ggplot(aes(x, -y, group = grp, fill=country)) +
geom_polygon(col='black', size=.3, alpha=.5) +
coord_equal() + guides(fill=F)
Я не знаком с R, но мне кажется, что вы делите путь на группы, ища команды closepath, а затем берете первый moveto в каждой группе в качестве отправной точки для суммирования позиций для преобразования в абсолютные. Два источника ошибок: 1. Команды moveto, кроме первой, также могут быть относительными (к последней координате предыдущей группы). 2. Группы нельзя закрывать командой closepath. Надежнее было бы поискать открывающийся moveto.
Привет @ccprog. Я использую closepath для создания переменной grp (которая идентифицирует уникальные многоугольники), но она не играет никакой роли в анализе фактических координат. На самом деле я просто использую поле SVG relative, которое, как я понимаю, указывает, когда координаты являются относительными или абсолютными. С абсолютными кодами вы также должны учитывать команды H / V, которые наследуют неактивную координату из предыдущей точки.





Посмотрите на вашу визуализацию Канады, особенно южного побережья Гудзонского пролива. Это очень очевидная ошибка. Просеивая данные пути, я обнаружил следующую последовательность в исходных данных:
h-2.28l-.91-.6-.92-.59H188.65l-1.03.02-.74-.07-.75-.07-.74-.07-.74-.06.88 1.09
Я загрузил ваш результат рендеринга в Inkscape и нарисовал соответствующую часть пути вверху, стрелкой отмечен сегмент, нарисованный абсолютной командой H. (Команда z была удалена, что является причиной отсутствия сегмента.) Очевидно, что где-то там сегмент слишком длинный.
Получается абсолютная Hисправляет предыдущей (горизонтальной) ошибки. Посмотрите на предыдущий пункт: это 198., 143., но это должен быть 191.76,146.07. Вертикальная ошибка остается на уровне -3,6.
Я сделал кодовый ключ, который максимально точно перекрывает исходные данные пути с вашим рендерингом. Данные пути были разделены на группы (одноугольники) и преобразованы в абсолютные с помощью Inkscape. К сожалению, программа не может преобразовать их в полигональные примитивы, поэтому там все еще есть команды V и H.
Он показывает это:
group0, все они удалены из предполагаемого места.Я сделал несколько визуальных измерений этого отклонения (ошибка ~ 0,05), и они в конечном итоге дали ключ к разгадке:
group01: 0.44,-0.73
group02: 0.84,-1.12
group03: 2.04,-1.44
group04: 2.94,-1.73
group05: 2.60,-1.86
group06: 3.14,-2.38
group07: 3.68,-2.54
group08: 4.03,-3.35
group09: 4.87,-2.97
group10: 6.08,-3.50 (begin)
group10: 0.00,-3.53 (end)
group11: 1.08,-1.95
group12: 2.05,-2.45
group13: 2.89,-2.84
group14: 3.64,-3.67
group15: 4.48,-3.44
group16: 4.04,-3.99
group17: 4.32,-3.08
group18: 4.75,-2.75
group19: 5.72,-2.95
group20: 5.40,-3.11
group21: 6.02,-2.95
group22: 6.63,-4.14
group23: 6.85,-5.00
group24: 7.14,-4.86
group25: 7.72,-4.39
group26: 8.65,-4.75
group27: 9.49,-4.39
group28: 10.20,-4.44
group29: 11.13,-4.58
Вы удаляете команды closepath, а затем вычисляете первую точку следующей группы относительно последней явной точки последней группы. Но closepath фактически перемещает текущую точку: обратно в позицию последней команды moveto. Они могут, но не обязательно, быть идентичными.
Я не могу дать вам готовый скрипт на R, но вам нужно сделать следующее: в начале новой группы кешировать позицию первой точки. В начале следующей группы вычислите новую первую точку относительно этой кэшированной точки.
Спасибо за помощь ccprog. Хорошо бы сосредоточиться на таких деталях. Так что это правда, что я установил значения NA для переменной y команд H равными нулю. Но позже я отменяю это с помощью if (d2$code[i] == 'H') d2$y_adj[i] = d2$y_adj[i-1] - в основном наследую предыдущее значение y. Я включаю соответствующие разделы data.frame для d и d2 - добавлено к вопросу.
Я понимаю, но я абсолютно уверен, что виновата абсолютная команда H. Я добавил скриншот, чтобы доказать свою точку зрения.
Нет, абсолютная H исправляет (горизонтальная) ошибка. Посмотрите на предыдущий пункт: это 198., 143., но это должен быть 191.76,146.07. Вертикальная ошибка сохраняется. Если, кроме того, я учту вертикальную ошибку и перемещаю ваш рендеринг на dy = -3.6, самая первая точка данных пути совпадает. Насколько я могу судить, все остальные группы путей внутренне непротиворечивы, но чем ниже они находятся в данных пути, тем больше они отклоняются от левого нижнего угла.
Это потребует некоторого размышления. Скоро вернусь.
Я сделал кодовый ключ, который максимально точно перекрывает исходные данные пути с вашим рендерингом. Данные пути были разделены на группы (одноугольники) и преобразованы в абсолютные с помощью Inkscape. К сожалению, программа не может преобразовать их в полигональные примитивы, поэтому там все еще есть команды V и H.
Я запустил график с более высоким разрешением, который должен помочь прояснить точную природу аномалии - imgur.com/a/nsyDOnU
Теперь я нашел ошибку. Это однозначный ответ.
Ты. Маленький. Потрошитель. Это должно быть так! Собираюсь усложнить свой код, но займусь этим.
... и теперь я прокручиваю эта страница вниз и вижу, что у парсера есть опция для преобразования путей в абсолютные команды. Это должно облегчить вам жизнь.
Хо-хо, какой-то недосмотр! Хорошо, это все значительно упрощает. Вызов узла console.info становится console.info(JSON.stringify(parseSVG.makeAbsolute(parseSVG(d)))), и я группируюсь с mutate(grp = (command == 'moveto') %>% cumsum).
Вы очень помогли ccprog - спасибо. Это было весело.
И это тоже полезно для меня, потому что, хотя мне не нужно рисовать Канаду, мне нужно разбирать пути SVG в R. Я только что кое-что узнал.
Я также отправил это в модуль svg-path-parser на github