IV/wm.sql (19047B) - Raw
1 \set ON_ERROR_STOP on 2 SET plpgsql.extra_errors TO 'all'; 3 4 -- wm_detect_bends detects bends using the inflection angles. No corrections. 5 drop function if exists wm_detect_bends; 6 create function wm_detect_bends( 7 line geometry, 8 dbgname text default null, 9 dbggen integer default null, 10 OUT bends geometry[] 11 ) as $$ 12 declare 13 pi constant real default radians(180); 14 p geometry; 15 p1 geometry; 16 p2 geometry; 17 p3 geometry; 18 bend geometry; 19 prev_sign int4; 20 cur_sign int4; 21 l_type text; 22 dbgpolygon geometry; 23 begin 24 l_type = st_geometrytype(line); 25 if l_type != 'ST_LineString' then 26 raise 'This function works with ST_LineString, got %', l_type; 27 end if; 28 29 -- The last vertex is iterated over twice, because the algorithm uses 3 30 -- vertices to calculate the angle between them. 31 -- 32 -- Given 3 vertices p1, p2, p3: 33 -- 34 -- p1___ ... 35 -- / 36 -- ... _____/ 37 -- p3 p2 38 -- 39 -- When looping over the line, p1 will be head (lead) vertex, p2 will be the 40 -- measured angle, and p3 will be trailing. The line that will be added to 41 -- the bend will always be [p3,p2]. 42 -- So once the p1 becomes the last vertex, the loop terminates, and the 43 -- [p2,p1] line will not have a chance to be added. So the loop adds the last 44 -- vertex twice, so it has a chance to become p2, and be added to the bend. 45 for p in 46 (select geom from st_dumppoints(line) order by path[1] asc) 47 union all 48 (select geom from st_dumppoints(line) order by path[1] desc limit 1) 49 loop 50 p3 = p2; 51 p2 = p1; 52 p1 = p; 53 continue when p3 is null; 54 55 cur_sign = sign(pi - st_angle(p1, p2, p2, p3)); 56 57 if bend is null then 58 bend = st_makeline(p3, p2); 59 else 60 bend = st_linemerge(st_union(bend, st_makeline(p3, p2))); 61 end if; 62 63 if prev_sign + cur_sign = 0 then 64 if bend is not null then 65 bends = bends || bend; 66 end if; 67 bend = st_makeline(p3, p2); 68 end if; 69 prev_sign = cur_sign; 70 end loop; 71 72 -- the last line may be lost if there is no "final" inflection angle. Add it. 73 if (select count(1) >= 2 from st_dumppoints(bend)) then 74 bends = bends || bend; 75 end if; 76 77 if dbgname is not null then 78 for i in 1..array_length(bends, 1) loop 79 insert into wm_debug(stage, name, gen, nbend, way) values( 80 'bbends', 81 dbgname, 82 dbggen, 83 i, 84 bends[i] 85 ); 86 87 dbgpolygon = null; 88 if st_npoints(bends[i]) >= 3 then 89 dbgpolygon = st_makepolygon( 90 st_addpoint(bends[i], 91 st_startpoint(bends[i])) 92 ); 93 end if; 94 insert into wm_debug(stage, name, gen, nbend, way) values( 95 'bbends-polygon', 96 dbgname, 97 dbggen, 98 i, 99 dbgpolygon 100 ); 101 end loop; 102 end if; 103 end 104 $$ language plpgsql; 105 106 -- wm_fix_gentle_inflections moves bend endpoints following "Gentle Inflection 107 -- at End of a Bend" section. 108 -- 109 -- The text does not specify how many vertices can be "adjusted"; it can 110 -- equally be one or many. This function is adjusting many, as long as the 111 -- cumulative inflection angle small (see variable below). 112 -- 113 -- The implementation could be significantly optimized to avoid `st_reverse` 114 -- and array reversals, trading for complexity in wm_fix_gentle_inflections1. 115 drop function if exists wm_fix_gentle_inflections; 116 create function wm_fix_gentle_inflections( 117 INOUT bends geometry[], 118 dbgname text default null, 119 dbggen integer default null 120 ) as $$ 121 declare 122 len int4; 123 bends1 geometry[]; 124 dbgpolygon geometry; 125 begin 126 len = array_length(bends, 1); 127 128 bends = wm_fix_gentle_inflections1(bends); 129 for i in 1..len loop 130 bends1[i] = st_reverse(bends[len-i+1]); 131 end loop; 132 bends1 = wm_fix_gentle_inflections1(bends1); 133 134 for i in 1..len loop 135 bends[i] = st_reverse(bends1[len-i+1]); 136 end loop; 137 138 if dbgname is not null then 139 for i in 1..array_length(bends, 1) loop 140 insert into wm_debug(stage, name, gen, nbend, way) values( 141 'cinflections', 142 dbgname, 143 dbggen, 144 i, 145 bends[i] 146 ); 147 148 dbgpolygon = null; 149 if st_npoints(bends[i]) >= 3 then 150 dbgpolygon = st_makepolygon( 151 st_addpoint(bends[i], 152 st_startpoint(bends[i])) 153 ); 154 end if; 155 156 insert into wm_debug(stage, name, gen, nbend, way) values( 157 'cinflections-polygon', 158 dbgname, 159 dbggen, 160 i, 161 dbgpolygon 162 ); 163 end loop; 164 end if; 165 end 166 $$ language plpgsql; 167 168 -- wm_fix_gentle_inflections1 fixes gentle inflections of an array of lines in 169 -- one direction. An implementation detail of wm_fix_gentle_inflections. 170 drop function if exists wm_fix_gentle_inflections1; 171 create function wm_fix_gentle_inflections1(INOUT bends geometry[]) as $$ 172 declare 173 pi constant real default radians(180); 174 -- the threshold when the angle is still "small", so gentle inflections can 175 -- be joined 176 small_angle constant real default radians(45); 177 ptail geometry; -- tail point of tail bend 178 phead geometry[]; -- 3 tail points of head bend 179 i int4; -- bends[i] is the current head 180 begin 181 for i in 2..array_length(bends, 1) loop 182 -- Predicate: two bends will always share an edge. Assuming (A,B,C,D,E,F) 183 -- is a bend: 184 -- C________D 185 -- / \ 186 -- \________/ \_______/ 187 -- A B E F 188 -- 189 -- Then edges (A,B) and (E,F) are shared with the neighboring bends. 190 -- 191 -- 192 -- Assume this curve (figure `inflection-1`), going clockwise from A: 193 -- 194 -- \______B 195 -- A `-------. C 196 -- | 197 -- G___ F | 198 -- / `-----.____+ D 199 -- E 200 -- 201 -- After processing the curve following the definition of a bend, the bend 202 -- [A-E] would be detected. Assuming inflection point E and F are "small", 203 -- the bend needs to be extended by two edges to [A,G]. 204 select geom from st_dumppoints(bends[i-1]) 205 order by path[1] asc limit 1 into ptail; 206 207 while true loop 208 -- copy last 3 points of bends[i-1] (tail) to ptail 209 select array( 210 select geom from st_dumppoints(bends[i]) order by path[1] asc limit 3 211 ) into phead; 212 213 -- if the bend got too short, stop processing it 214 exit when array_length(phead, 1) < 3; 215 216 -- inflection angle between ptail[1:3] is "large", stop processing 217 exit when abs(st_angle(phead[1], phead[2], phead[3]) - pi) > small_angle; 218 219 -- distance from head's 1st vertex should be larger than from 2nd vertex 220 exit when st_distance(ptail, phead[2]) < st_distance(ptail, phead[3]); 221 222 -- Detected a gentle inflection. 223 -- Move head of the tail to the tail of head 224 bends[i] = st_removepoint(bends[i], 0); 225 bends[i-1] = st_addpoint(bends[i-1], phead[3]); 226 end loop; 227 228 end loop; 229 end 230 $$ language plpgsql; 231 232 -- wm_if_selfcross returns whether baseline of bendi crosses bendj. 233 -- If it doesn't, returns a null geometry. 234 -- Otherwise, it will return the baseline split into a few parts where it 235 -- crosses bendj. 236 drop function if exists wm_if_selfcross; 237 create function wm_if_selfcross( 238 bendi geometry, 239 bendj geometry 240 ) returns geometry as $$ 241 declare 242 a geometry; 243 b geometry; 244 multi geometry; 245 begin 246 a = st_pointn(bendi, 1); 247 b = st_pointn(bendi, -1); 248 multi = st_split(bendj, st_makeline(a, b)); 249 250 if st_numgeometries(multi) = 1 then 251 return null; 252 end if; 253 254 if st_numgeometries(multi) = 2 and 255 (st_contains(bendj, a) or st_contains(bendj, b)) then 256 return null; 257 end if; 258 259 return multi; 260 end 261 $$ language plpgsql; 262 263 264 -- wm_self_crossing eliminates self-crossing from the bends, following the 265 -- article's section "Self-line Crossing When Cutting a Bend". 266 drop function if exists wm_self_crossing; 267 create function wm_self_crossing( 268 INOUT bends geometry[], 269 dbgname text default null, 270 dbggen integer default null, 271 OUT mutated boolean 272 ) as $$ 273 declare 274 pi constant real default radians(180); 275 i int4; 276 j int4; 277 multi geometry; 278 begin 279 mutated = false; 280 <<bendloop>> 281 for i in 1..array_length(bends, 1) loop 282 continue when abs(wm_inflection_angle(bends[i])) <= pi; 283 -- sum of inflection angles for this bend is >180, so it may be 284 -- self-crossing. Now try to find another bend in this line that 285 -- crosses an imaginary line of end-vertices 286 287 -- Go through each bend in the given line, and see if has a potential to 288 -- cross bends[i]. The line-cut process is different when i<j and i>j; 289 -- therefore there are two loops, one for each case. 290 for j in 1..i-1 loop 291 multi = wm_if_selfcross(bends[i], bends[j]); 292 continue when multi is null; 293 mutated = true; 294 295 -- remove first vertex of the following bend, because the last 296 -- segment is always duplicated with the i'th bend. 297 bends[i+1] = st_removepoint(bends[i+1], 0); 298 bends[j] = st_geometryn(multi, 1); 299 bends[j] = st_setpoint( 300 bends[j], 301 st_npoints(bends[j])-1, 302 st_pointn(bends[i], st_npoints(bends[i])) 303 ); 304 bends = bends[1:j] || bends[i+1:]; 305 continue bendloop; 306 end loop; 307 308 for j in reverse array_length(bends, 1)..i+1 loop 309 multi = wm_if_selfcross(bends[i], bends[j]); 310 continue when multi is null; 311 mutated = true; 312 313 -- remove last vertex of the previous bend, because the last 314 -- segment is duplicated with the i'th bend. 315 bends[i-1] = st_removepoint(bends[i-1], st_npoints(bends[i-1])-1); 316 bends[i] = st_makeline( 317 st_pointn(bends[i], 1), 318 st_removepoint(st_geometryn(multi, st_numgeometries(multi)), 0) 319 ); 320 bends = bends[1:i] || bends[j+1:]; 321 continue bendloop; 322 end loop; 323 end loop; 324 325 if dbgname is not null then 326 insert into wm_debug(stage, name, gen, nbend, way) values( 327 'dcrossings', 328 dbgname, 329 dbggen, 330 generate_subscripts(bends, 1), 331 unnest(bends) 332 ); 333 end if; 334 end 335 $$ language plpgsql; 336 337 drop function if exists wm_inflection_angle; 338 create function wm_inflection_angle (IN bend geometry, OUT angle real) as $$ 339 declare 340 pi constant real default radians(180); 341 p0 geometry; 342 p1 geometry; 343 p2 geometry; 344 p3 geometry; 345 begin 346 angle = 0; 347 for p0 in select geom from st_dumppoints(bend) order by path[1] asc loop 348 p3 = p2; 349 p2 = p1; 350 p1 = p0; 351 continue when p3 is null; 352 angle = angle + abs(pi - st_angle(p1, p2, p3)); 353 end loop; 354 end 355 $$ language plpgsql; 356 357 drop function if exists wm_bend_attrs; 358 drop function if exists wm_isolated_bends; 359 drop function if exists wm_elimination; 360 drop type if exists wm_t_bend_attrs; 361 create type wm_t_bend_attrs as ( 362 bend geometry, 363 area real, 364 cmp real, 365 adjsize real, 366 baselinelength real, 367 curvature real, 368 isolated boolean 369 ); 370 create function wm_bend_attrs( 371 bends geometry[], 372 dbgname text default null, 373 dbggen integer default null 374 ) returns setof wm_t_bend_attrs as $$ 375 declare 376 fourpi constant real default 4*radians(180); 377 i int4; 378 polygon geometry; 379 bend geometry; 380 res wm_t_bend_attrs; 381 begin 382 for i in 1..array_length(bends, 1) loop 383 bend = bends[i]; 384 res = null; 385 res.bend = bend; 386 res.area = 0; 387 res.cmp = 0; 388 res.adjsize = 0; 389 res.baselinelength = st_distance(st_startpoint(bend), st_endpoint(bend)); 390 res.curvature = wm_inflection_angle(bend) / st_length(bend); 391 res.isolated = false; 392 if st_numpoints(bend) >= 3 then 393 polygon = st_makepolygon(st_addpoint(bend, st_startpoint(bend))); 394 -- Compactness Index (cmp) is defined as "the ratio of the area of the 395 -- polygon over the circle whose circumference length is the same as the 396 -- length of the circumference of the polygon". I assume they meant the 397 -- area of the circle. So here goes: 398 -- 1. get polygon area P. 399 -- 2. get polygon perimeter = u. Pretend it's our circle's circumference. 400 -- 3. get A (area) of the circle from u: A = u^2/(4pi) 401 -- 4. divide P by A: cmp = P/A = P/(u^2/(4pi)) = 4pi*P/u^2 402 res.area = st_area(polygon); 403 res.cmp = fourpi*res.area/(st_perimeter(polygon)^2); 404 if res.cmp > 0 then 405 res.adjsize = (res.area*(0.75/res.cmp)); 406 end if; 407 end if; 408 409 if dbgname is not null then 410 insert into wm_debug (stage, name, gen, nbend, way, props) values( 411 'ebendattrs', 412 dbgname, 413 dbggen, 414 i, 415 bend, 416 jsonb_build_object( 417 'area', res.area, 418 'cmp', res.cmp, 419 'adjsize', res.adjsize, 420 'baselinelength', res.baselinelength, 421 'curvature', res.curvature 422 ) 423 ); 424 end if; 425 return next res; 426 end loop; 427 end; 428 $$ language plpgsql; 429 430 create function wm_elimination( 431 INOUT bendattrs wm_t_bend_attrs[], 432 dhalfcircle float, 433 dbgname text default null, 434 dbggen integer default null, 435 OUT mutated boolean 436 ) as $$ 437 declare 438 area_threshold float; 439 leftsize float; 440 rightsize float; 441 i int4; 442 j int4; 443 tmpbendattrs wm_t_bend_attrs; 444 dbgbends geometry[]; 445 begin 446 area_threshold = radians(180) * ((dhalfcircle/2)^2)/2; 447 mutated = false; 448 449 i = 1; 450 while i < array_length(bendattrs, 1)-1 loop 451 i = i + 1; 452 continue when bendattrs[i].adjsize = 0; 453 continue when bendattrs[i].adjsize > area_threshold; 454 455 if i = 2 then 456 leftsize = bendattrs[i].adjsize + 1; 457 else 458 leftsize = bendattrs[i-1].adjsize; 459 end if; 460 461 if i = array_length(bendattrs, 1)-1 then 462 rightsize = bendattrs[i].adjsize + 1; 463 else 464 rightsize = bendattrs[i+1].adjsize; 465 end if; 466 467 continue when bendattrs[i].adjsize >= leftsize; 468 continue when bendattrs[i].adjsize >= rightsize; 469 470 -- Local minimum. Elminate bend! 471 mutated = true; 472 tmpbendattrs.bend = st_makeline( 473 st_pointn(bendattrs[i].bend, 1), 474 st_pointn(bendattrs[i].bend, -1) 475 ); 476 bendattrs[i] = tmpbendattrs; 477 -- remove last vertex of the previous bend and 478 -- first vertex of the next bend, because bends always 479 -- share a line segment together 480 tmpbendattrs.bend = st_removepoint( 481 bendattrs[i-1].bend, 482 st_npoints(bendattrs[i-1].bend)-1 483 ); 484 bendattrs[i-1] = tmpbendattrs; 485 tmpbendattrs.bend = st_removepoint(bendattrs[i+1].bend, 0); 486 bendattrs[i+1] = tmpbendattrs; 487 -- the next bend's adjsize is now messed up; it should not be taken 488 -- into consideration for other local minimas. Skip over 2. 489 i = i + 2; 490 end loop; 491 492 if dbgname is not null then 493 for j in 1..array_length(bendattrs, 1) loop 494 dbgbends[j] = bendattrs[j].bend; 495 end loop; 496 497 insert into wm_debug(stage, name, gen, nbend, way) values( 498 'felimination', 499 dbgname, 500 dbggen, 501 generate_subscripts(dbgbends, 1), 502 unnest(dbgbends) 503 ); 504 end if; 505 end 506 $$ language plpgsql; 507 508 create function wm_isolated_bends( 509 INOUT bendattrs wm_t_bend_attrs[], 510 dbgname text default null, 511 dbggen integer default null 512 ) as $$ 513 declare 514 -- if neighbor's curvatures are within this fraction of the current bend 515 isolation_threshold constant real default 0.5; 516 this real; 517 skip_next bool; 518 res wm_t_bend_attrs; 519 i int4; 520 begin 521 for i in 2..array_length(bendattrs, 1)-1 loop 522 res = bendattrs[i]; 523 if skip_next then 524 skip_next = false; 525 else 526 this = bendattrs[i].curvature * isolation_threshold; 527 if bendattrs[i-1].curvature < this and 528 bendattrs[i+1].curvature < this then 529 res.isolated = true; 530 bendattrs[i] = res; 531 skip_next = true; 532 end if; 533 end if; 534 535 if dbgname is not null then 536 insert into wm_debug (stage, name, gen, nbend, way, props) values( 537 'fisolated_bends', 538 dbgname, 539 dbggen, 540 i, 541 res.bend, 542 jsonb_build_object( 543 'area', res.area, 544 'cmp', res.cmp, 545 'adjsize', res.adjsize, 546 'baselinelength', res.baselinelength, 547 'curvature', res.curvature, 548 'isolated', res.isolated 549 ) 550 ); 551 end if; 552 553 end loop; 554 end 555 $$ language plpgsql; 556 557 drop function if exists ST_SimplifyWM_Estimate; 558 create function ST_SimplifyWM_Estimate( 559 geom geometry, 560 OUT npoints bigint, 561 OUT secs bigint 562 ) as $$ 563 declare 564 lines geometry[]; 565 l_type text; 566 begin 567 l_type = st_geometrytype(geom); 568 if l_type = 'ST_LineString' then 569 lines = array[geom]; 570 elseif l_type = 'ST_MultiLineString' then 571 lines = array((select a.geom from st_dump(geom) a order by path[1] asc)); 572 else 573 raise 'Unknown geometry type %', l_type; 574 end if; 575 576 npoints = 0; 577 for i in 1..array_length(lines, 1) loop 578 npoints = npoints + st_numpoints(lines[i]); 579 end loop; 580 secs = npoints / 150; 581 end 582 $$ language plpgsql; 583 584 -- ST_SimplifyWM simplifies a given geometry using Wang & Müller's 585 -- "Line Generalization Based on Analysis of Shape Characteristics" algorithm, 586 -- 1998. 587 -- Input parameters: 588 -- - geom: ST_LineString or ST_MultiLineString: the geometry to be simplified 589 -- - dhalfcircle: the diameter of a half-circle, whose area is an approximate 590 -- threshold for small bend elimination. If bend's area is larger than that, 591 -- the bend will be left alone. 592 drop function if exists ST_SimplifyWM; 593 create function ST_SimplifyWM( 594 geom geometry, 595 dhalfcircle float, 596 dbgname text default null 597 ) returns geometry as $$ 598 declare 599 gen integer; 600 i integer; 601 j integer; 602 line geometry; 603 lines geometry[]; 604 bends geometry[]; 605 bendattrs wm_t_bend_attrs[]; 606 mutated boolean; 607 l_type text; 608 begin 609 l_type = st_geometrytype(geom); 610 if l_type = 'ST_LineString' then 611 lines = array[geom]; 612 elseif l_type = 'ST_MultiLineString' then 613 lines = array((select a.geom from st_dump(geom) a order by path[1] asc)); 614 else 615 raise 'Unknown geometry type %', l_type; 616 end if; 617 618 for i in 1..array_length(lines, 1) loop 619 mutated = true; 620 gen = 1; 621 while mutated loop 622 if dbgname is not null then 623 insert into wm_debug (stage, name, gen, nbend, way) values( 624 'afigures', dbgname, gen, i, lines[i]); 625 end if; 626 627 bends = wm_detect_bends(lines[i], dbgname, gen); 628 bends = wm_fix_gentle_inflections(bends, dbgname, gen); 629 630 select * from wm_self_crossing(bends, dbgname, gen) into bends, mutated; 631 632 if mutated then 633 lines[i] = st_linemerge(st_union(bends)); 634 gen = gen + 1; 635 continue; 636 end if; 637 638 bendattrs = array((select wm_bend_attrs(bends, dbgname, gen))); 639 640 select * from wm_elimination( 641 bendattrs, dhalfcircle, dbgname, gen) into bendattrs, mutated; 642 if mutated then 643 for j in 1..array_length(bendattrs, 1) loop 644 bends[j] = bendattrs[j].bend; 645 end loop; 646 lines[i] = st_linemerge(st_union(bends)); 647 gen = gen + 1; 648 continue; 649 end if; 650 651 -- code to detect isolated bends is there, but bend exaggeration 652 -- is not implemented. 653 --perform wm_isolated_bends(bendattrs, dbgname, gen); 654 end loop; 655 656 end loop; 657 658 if l_type = 'ST_LineString' then 659 return st_linemerge(st_union(lines)); 660 elseif l_type = 'ST_MultiLineString' then 661 return st_union(lines); 662 end if; 663 end 664 $$ language plpgsql;