From 5953ec70322fcd4c4c6e6397de745283fe8ed866 Mon Sep 17 00:00:00 2001 From: James R Swift Date: Mon, 29 Jul 2024 14:12:56 +0100 Subject: [PATCH 1/9] Refactor axes part 1 --- src/axes.typ | 908 ++++--------------------------- src/axes/draw.typ | 64 +++ src/axes/grid.typ | 45 ++ src/axes/preset.typ | 2 + src/axes/presets/school-book.typ | 139 +++++ src/axes/presets/scientific.typ | 153 ++++++ src/axes/style.typ | 117 ++++ src/axes/ticks.typ | 251 +++++++++ src/axes/util.typ | 0 src/axes/viewport.typ | 78 +++ src/plot.typ | 2 +- src/plot/util.typ | 99 ---- 12 files changed, 944 insertions(+), 914 deletions(-) create mode 100644 src/axes/draw.typ create mode 100644 src/axes/grid.typ create mode 100644 src/axes/preset.typ create mode 100644 src/axes/presets/school-book.typ create mode 100644 src/axes/presets/scientific.typ create mode 100644 src/axes/style.typ create mode 100644 src/axes/ticks.typ create mode 100644 src/axes/util.typ create mode 100644 src/axes/viewport.typ diff --git a/src/axes.typ b/src/axes.typ index 04d404b..981ada7 100644 --- a/src/axes.typ +++ b/src/axes.typ @@ -1,219 +1,6 @@ #import "/src/cetz.typ": util, draw, vector, matrix, styles, process, drawable, path-util, process - -#let typst-content = content - -/// Default axis style -/// -/// #show-parameter-block("tick-limit", "int", default: 100, [Upper major tick limit.]) -/// #show-parameter-block("minor-tick-limit", "int", default: 1000, [Upper minor tick limit.]) -/// #show-parameter-block("auto-tick-factors", "array", [List of tick factors used for automatic tick step determination.]) -/// #show-parameter-block("auto-tick-count", "int", [Number of ticks to generate by default.]) -/// #show-parameter-block("stroke", "stroke", [Axis stroke style.]) -/// #show-parameter-block("label.offset", "number", [Distance to move axis labels away from the axis.]) -/// #show-parameter-block("label.anchor", "anchor", [Anchor of the axis label to use for it's placement.]) -/// #show-parameter-block("label.angle", "angle", [Angle of the axis label.]) -/// #show-parameter-block("axis-layer", "float", [Layer to draw axes on (see @@on-layer() )]) -/// #show-parameter-block("grid-layer", "float", [Layer to draw the grid on (see @@on-layer() )]) -/// #show-parameter-block("background-layer", "float", [Layer to draw the background on (see @@on-layer() )]) -/// #show-parameter-block("padding", "number", [Extra distance between axes and plotting area. For schoolbook axes, this is the length of how much axes grow out of the plotting area.]) -/// #show-parameter-block("overshoot", "number", [School-book style axes only: Extra length to add to the end (right, top) of axes.]) -/// #show-parameter-block("tick.stroke", "stroke", [Major tick stroke style.]) -/// #show-parameter-block("tick.minor-stroke", "stroke", [Minor tick stroke style.]) -/// #show-parameter-block("tick.offset", ("number", "ratio"), [Major tick offset along the tick's direction, can be relative to the length.]) -/// #show-parameter-block("tick.minor-offset", ("number", "ratio"), [Minor tick offset along the tick's direction, can be relative to the length.]) -/// #show-parameter-block("tick.length", ("number"), [Major tick length.]) -/// #show-parameter-block("tick.minor-length", ("number", "ratio"), [Minor tick length, can be relative to the major tick length.]) -/// #show-parameter-block("tick.label.offset", ("number"), [Major tick label offset away from the tick.]) -/// #show-parameter-block("tick.label.angle", ("angle"), [Major tick label angle.]) -/// #show-parameter-block("tick.label.anchor", ("anchor"), [Anchor of major tick labels used for positioning.]) -/// #show-parameter-block("tick.label.show", ("auto", "bool"), default: auto, [Set visibility of tick labels. A value of `auto` shows tick labels for all but mirrored axes.]) -/// #show-parameter-block("grid.stroke", "stroke", [Major grid line stroke style.]) -/// #show-parameter-block("break-point.width", "number", [Axis break width along the axis.]) -/// #show-parameter-block("break-point.length", "number", [Axis break length.]) -/// #show-parameter-block("minor-grid.stroke", "stroke", [Minor grid line stroke style.]) -/// #show-parameter-block("shared-zero", ("bool", "content"), default: "$0$", [School-book style axes only: Content to display at the plots origin (0,0). If set to `false`, nothing is shown. Having this set, suppresses auto-generated ticks for $0$!]) -#let default-style = ( - tick-limit: 100, - minor-tick-limit: 1000, - auto-tick-factors: (1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10), // Tick factor to try - auto-tick-count: 11, // Number of ticks the plot tries to place - fill: none, - stroke: auto, - label: ( - offset: .2cm, // Axis label offset - anchor: auto, // Axis label anchor - angle: auto, // Axis label angle - ), - axis-layer: 0, - grid-layer: 0, - background-layer: 0, - padding: 0, - tick: ( - fill: none, - stroke: black + 1pt, - minor-stroke: black + .5pt, - offset: 0, - minor-offset: 0, - length: .1cm, // Tick length: Number - minor-length: 70%, // Minor tick length: Number, Ratio - label: ( - offset: .15cm, // Tick label offset - angle: 0deg, // Tick label angle - anchor: auto, // Tick label anchor - "show": auto, // Show tick labels for axes in use - ) - ), - break-point: ( - width: .75cm, - length: .15cm, - ), - grid: ( - stroke: (paint: gray.lighten(50%), thickness: 1pt), - ), - minor-grid: ( - stroke: (paint: gray.lighten(50%), thickness: .5pt), - ), -) - -// Default Scientific Style -#let default-style-scientific = util.merge-dictionary(default-style, ( - left: (tick: (label: (anchor: "east"))), - bottom: (tick: (label: (anchor: "north"))), - right: (tick: (label: (anchor: "west"))), - top: (tick: (label: (anchor: "south"))), - stroke: (cap: "square"), - padding: 0, -)) - -#let default-style-schoolbook = util.merge-dictionary(default-style, ( - x: (stroke: auto, fill: none, mark: (start: none, end: "straight"), - tick: (label: (anchor: "north"))), - y: (stroke: auto, fill: none, mark: (start: none, end: "straight"), - tick: (label: (anchor: "east"))), - label: (offset: .1cm), - origin: (label: (offset: .05cm)), - padding: .1cm, // Axis padding on both sides outsides the plotting area - overshoot: .5cm, // Axis end "overshoot" out of the plotting area - tick: ( - offset: -50%, - minor-offset: -50%, - length: .2cm, - minor-length: 70%, - ), - shared-zero: $0$, // Show zero tick label at (0, 0) -)) - -#let _prepare-style(ctx, style) = { - if type(style) != dictionary { return style } - - let res = util.resolve-number.with(ctx) - let rel-to(v, to) = { - if type(v) == ratio { - return v * to / 100% - } else { - return res(v) - } - } - - style.tick.length = res(style.tick.length) - style.tick.offset = rel-to(style.tick.offset, style.tick.length) - style.tick.minor-length = rel-to(style.tick.minor-length, style.tick.length) - style.tick.minor-offset = rel-to(style.tick.minor-offset, style.tick.minor-length) - style.tick.label.offset = res(style.tick.label.offset) - - // Break points - style.break-point.width = res(style.break-point.width) - style.break-point.length = res(style.break-point.length) - - // Padding - style.padding = res(style.padding) - - if "overshoot" in style { - style.overshoot = res(style.overshoot) - } - - return style -} - -#let _get-axis-style(ctx, style, name) = { - if not name in style { - return style - } - - style = styles.resolve(style, merge: style.at(name)) - return _prepare-style(ctx, style) -} - -#let _get-grid-type(axis) = { - let grid = axis.ticks.at("grid", default: false) - if grid == "major" or grid == true { return 1 } - if grid == "minor" { return 2 } - if grid == "both" { return 3 } - return 0 -} - -#let _inset-axis-points(ctx, style, axis, start, end) = { - if axis == none { return (start, end) } - - let (low, high) = axis.inset.map(v => util.resolve-number(ctx, v)) - - let is-horizontal = start.at(1) == end.at(1) - if is-horizontal { - start = vector.add(start, (low, 0)) - end = vector.sub(end, (high, 0)) - } else { - start = vector.add(start, (0, low)) - end = vector.sub(end, (0, high)) - } - return (start, end) -} - -#let _draw-axis-line(start, end, axis, is-horizontal, style) = { - let enabled = if axis != none and axis.show-break { - axis.min > 0 or axis.max < 0 - } else { false } - - if enabled { - let size = if is-horizontal { - (style.break-point.width, 0) - } else { - (0, style.break-point.width, 0) - } - - let up = if is-horizontal { - (0, style.break-point.length) - } else { - (style.break-point.length, 0) - } - - let add-break(is-end) = { - let a = () - let b = (rel: vector.scale(size, .3), update: false) - let c = (rel: vector.add(vector.scale(size, .4), vector.scale(up, -1)), update: false) - let d = (rel: vector.add(vector.scale(size, .6), vector.scale(up, +1)), update: false) - let e = (rel: vector.scale(size, .7), update: false) - let f = (rel: size) - - let mark = if is-end { - style.at("mark", default: none) - } - draw.line(a, b, c, d, e, f, stroke: style.stroke, mark: mark) - } - - draw.merge-path({ - draw.move-to(start) - if axis.min > 0 { - add-break(false) - draw.line((rel: size, to: start), end, mark: style.at("mark", default: none)) - } else if axis.max < 0 { - draw.line(start, (rel: vector.scale(size, -1), to: end)) - add-break(true) - } - }, stroke: style.stroke) - } else { - draw.line(start, end, stroke: style.stroke, mark: style.at("mark", default: none)) - } -} +#import "axes/style.typ": _prepare-style, _get-axis-style +#import "axes/preset.typ" // Construct Axis Object // @@ -232,216 +19,12 @@ min: min, max: max, ticks: ticks, label: label, inset: (0, 0), show-break: false, ) -// Format a tick value -#let format-tick-value(value, tic-options) = { - // Without it we get negative zero in conversion - // to content! Typst has negative zero floats. - if value == 0 { value = 0 } - - let round(value, digits) = { - calc.round(value, digits: digits) - } - - let format-float(value, digits) = { - $#round(value, digits)$ - } - - let format-sci(value, digits) = { - let exponent = if value != 0 { - calc.floor(calc.log(calc.abs(value), base: 10)) - } else { - 0 - } - - let ee = calc.pow(10, calc.abs(exponent + 1)) - if exponent > 0 { - value = value / ee * 10 - } else if exponent < 0 { - value = value * ee * 10 - } - - value = round(value, digits) - if exponent <= -1 or exponent >= 1 { - return $#value times 10^#exponent$ - } - return $#value$ - } - - if type(value) != typst-content { - let format = tic-options.at("format", default: "float") - if format == none { - value = [] - } else if type(format) == typst-content { - value = format - } else if type(format) == function { - value = (format)(value) - } else if format == "sci" { - value = format-sci(value, tic-options.at("decimals", default: 2)) - } else { - value = format-float(value, tic-options.at("decimals", default: 2)) - } - } else if type(value) != typst-content { - value = str(value) - } - - if tic-options.at("unit", default: none) != none { - value += tic-options.unit - } - return value -} - -// Get value on axis [0, 1] -// -// - axis (axis): Axis -// - v (number): Value -// -> float -#let value-on-axis(axis, v) = { - if v == none { return } - let (min, max) = (axis.min, axis.max) - let dt = max - min; if dt == 0 { dt = 1 } - - return (v - min) / dt -} - -// Compute list of linear ticks for axis -// -// - axis (axis): Axis -#let compute-linear-ticks(axis, style, add-zero: true) = { - let (min, max) = (axis.min, axis.max) - let dt = max - min; if (dt == 0) { dt = 1 } - let ticks = axis.ticks - let ferr = util.float-epsilon - let tick-limit = style.tick-limit - let minor-tick-limit = style.minor-tick-limit - - let l = () - if ticks != none { - let major-tick-values = () - if "step" in ticks and ticks.step != none { - assert(ticks.step >= 0, - message: "Axis tick step must be positive and non 0.") - if axis.min > axis.max { ticks.step *= -1 } - - let s = 1 / ticks.step - - let num-ticks = int(max * s + 1.5) - int(min * s) - assert(num-ticks <= tick-limit, - message: "Number of major ticks exceeds limit " + str(tick-limit)) - - let n = range(int(min * s), int(max * s + 1.5)) - for t in n { - let v = (t / s - min) / dt - if t / s == 0 and not add-zero { continue } - - if v >= 0 - ferr and v <= 1 + ferr { - l.push((v, format-tick-value(t / s, ticks), true)) - major-tick-values.push(v) - } - } - } - - if "minor-step" in ticks and ticks.minor-step != none { - assert(ticks.minor-step >= 0, - message: "Axis minor tick step must be positive") - if axis.min > axis.max { ticks.minor-step *= -1 } - - let s = 1 / ticks.minor-step - - let num-ticks = int(max * s + 1.5) - int(min * s) - assert(num-ticks <= minor-tick-limit, - message: "Number of minor ticks exceeds limit " + str(minor-tick-limit)) - - let n = range(int(min * s), int(max * s + 1.5)) - for t in n { - let v = (t / s - min) / dt - if v in major-tick-values { - // Prefer major ticks over minor ticks - continue - } - - if v != none and v >= 0 and v <= 1 + ferr { - l.push((v, none, false)) - } - } - } - - } - - return l -} - -// Get list of fixed axis ticks -// -// - axis (axis): Axis object -#let fixed-ticks(axis) = { - let l = () - if "list" in axis.ticks { - for t in axis.ticks.list { - let (v, label) = (none, none) - if type(t) in (float, int) { - v = t - label = format-tick-value(t, axis.ticks) - } else { - (v, label) = t - } - - v = value-on-axis(axis, v) - if v != none and v >= 0 and v <= 1 { - l.push((v, label, true)) - } - } - } - return l -} - -// Compute list of axis ticks -// -// A tick triple has the format: -// (rel-value: float, label: content, major: bool) -// -// - axis (axis): Axis object -#let compute-ticks(axis, style, add-zero: true) = { - let find-max-n-ticks(axis, n: 11) = { - let dt = calc.abs(axis.max - axis.min) - let scale = calc.floor(calc.log(dt, base: 10) - 1) - if scale > 5 or scale < -5 {return none} - - let (step, best) = (none, 0) - for s in style.auto-tick-factors { - s = s * calc.pow(10, scale) - - let divs = calc.abs(dt / s) - if divs >= best and divs <= n { - step = s - best = divs - } - } - return step - } - - if axis == none or axis.ticks == none { return () } - if axis.ticks.step == auto { - axis.ticks.step = find-max-n-ticks(axis, n: style.auto-tick-count) - } - if axis.ticks.minor-step == auto { - axis.ticks.minor-step = if axis.ticks.step != none { - axis.ticks.step / 5 - } else { - none - } - } - - let ticks = compute-linear-ticks(axis, style, add-zero: add-zero) - ticks += fixed-ticks(axis) - return ticks -} - // Prepares the axis post creation. The given axis // must be completely set-up, including its intervall. // Returns the prepared axis #let prepare-axis(ctx, axis, name) = { let style = styles.resolve(ctx.style, root: "axes", - base: default-style-scientific) + base: preset.scientific.default-style-scientific) style = _prepare-style(ctx, style) style = _get-axis-style(ctx, style, name) @@ -462,415 +45,112 @@ return axis } -// Transform a single vector along a x, y and z axis -// -// - size (vector): Coordinate system size -// - x-axis (axis): X axis -// - y-axis (axis): Y axis -// - z-axis (axis): Z axis -// - vec (vector): Input vector to transform -// -> vector -#let transform-vec(size, x-axis, y-axis, z-axis, vec) = { - let (ox, oy, ..) = (0, 0, 0) - ox += x-axis.inset.at(0) - oy += y-axis.inset.at(0) - - let (sx, sy) = size - sx -= x-axis.inset.sum() - sy -= y-axis.inset.sum() - - let x-range = x-axis.max - x-axis.min - let y-range = y-axis.max - y-axis.min - let z-range = 0 //z-axis.max - z-axis.min - - let fx = sx / x-range - let fy = sy / y-range - let fz = 0 //sz / z-range - - let x-low = calc.min(x-axis.min, x-axis.max) - let x-high = calc.max(x-axis.min, x-axis.max) - let y-low = calc.min(y-axis.min, y-axis.max) - let y-high = calc.max(y-axis.min, y-axis.max) - //let z-low = calc.min(z-axis.min, z-axis.max) - //let z-hihg = calc.max(z-axis.min, z-axis.max) - - let (x, y, ..) = vec - - return ( - (x - x-axis.min) * fx + ox, - (y - y-axis.min) * fy + oy, - 0) //(z - z-axis.min) * fz + oz) +// Get the default axis orientation +// depending on the axis name +#let get-default-axis-horizontal(name) = { + return lower(name).starts-with("x") } -// Draw inside viewport coordinates of two axes +// Setup axes dictionary // -// - size (vector): Axis canvas size (relative to origin) -// - x (axis): Horizontal axis -// - y (axis): Vertical axis -// - z (axis): Z axis -// - name (string,none): Group name -#let axis-viewport(size, x, y, z, body, name: none) = { - draw.group(name: name, (ctx => { - let transform = ctx.transform - - ctx.transform = matrix.ident() - let (ctx, drawables, bounds) = process.many(ctx, util.resolve-body(ctx, body)) - - ctx.transform = transform - - drawables = drawables.map(d => { - if "segments" in d { - d.segments = d.segments.map(((kind, ..pts)) => { - (kind, ..pts.map(pt => { - transform-vec(size, x, y, none, pt) - })) - }) - } - if "pos" in d { - d.pos = transform-vec(size, x, y, none, d.pos) +// - axis-dict (dictionary): Existing axis dictionary +// - options (dictionary): Named arguments +// - plot-size (tuple): Plot width, height tuple +#let setup-axes(ctx, axis-dict, options, plot-size) = { + import "/src/axes.typ" + + // Get axis option for name + let get-axis-option(axis-name, name, default) = { + let v = options.at(axis-name + "-" + name, default: default) + if v == auto { default } else { v } + } + + for (name, axis) in axis-dict { + if not "ticks" in axis { axis.ticks = () } + axis.label = get-axis-option(name, "label", $#name$) + + // Configure axis bounds + axis.min = get-axis-option(name, "min", axis.min) + axis.max = get-axis-option(name, "max", axis.max) + + assert(axis.min not in (none, auto) and + axis.max not in (none, auto), + message: "Axis min and max must be set.") + if axis.min == axis.max { + axis.min -= 1; axis.max += 1 + } + + // Configure axis orientation + axis.horizontal = get-axis-option(name, "horizontal", + get-default-axis-horizontal(name)) + + // Configure ticks + axis.ticks.list = get-axis-option(name, "ticks", ()) + axis.ticks.step = get-axis-option(name, "tick-step", axis.ticks.step) + axis.ticks.minor-step = get-axis-option(name, "minor-tick-step", axis.ticks.minor-step) + axis.ticks.decimals = get-axis-option(name, "decimals", 2) + axis.ticks.unit = get-axis-option(name, "unit", []) + axis.ticks.format = get-axis-option(name, "format", axis.ticks.format) + + // Axis break + axis.show-break = get-axis-option(name, "break", false) + axis.inset = get-axis-option(name, "inset", (0, 0)) + + // Configure grid + axis.ticks.grid = get-axis-option(name, "grid", false) + + axis-dict.at(name) = axis + } + + // Set axis options round two, after setting + // axis bounds + for (name, axis) in axis-dict { + let changed = false + + // Configure axis aspect ratio + let equal-to = get-axis-option(name, "equal", none) + if equal-to != none { + assert.eq(type(equal-to), str, + message: "Expected axis name.") + assert(equal-to != name, + message: "Axis can not be equal to itself.") + + let other = axis-dict.at(equal-to, default: none) + assert(other != none, + message: "Other axis must exist.") + assert(other.horizontal != axis.horizontal, + message: "Equal axes must have opposing orientation.") + + let (w, h) = plot-size + let ratio = if other.horizontal { + h / w + } else { + w / h } - return d - }) - - return ( - ctx: ctx, - drawables: drawable.apply-transform(ctx.transform, drawables) - ) - },)) -} - -// Draw grid lines for the ticks of an axis -// -// - cxt (context): -// - axis (dictionary): The axis -// - ticks (array): The computed ticks -// - low (vector): Start position of a grid-line at tick 0 -// - high (vector): End position of a grid-line at tick 0 -// - dir (vector): Normalized grid direction vector along the grid axis -// - style (style): Axis style -#let draw-grid-lines(ctx, axis, ticks, low, high, dir, style) = { - let offset = (0,0) - if axis.inset != none { - let (inset-low, inset-high) = axis.inset.map(v => util.resolve-number(ctx, v)) - offset = vector.scale(vector.norm(dir), inset-low) - dir = vector.sub(dir, vector.scale(vector.norm(dir), inset-low + inset-high)) - } - - let kind = _get-grid-type(axis) - if kind > 0 { - for (distance, label, is-major) in ticks { - let offset = vector.add(vector.scale(dir, distance), offset) - let start = vector.add(low, offset) - let end = vector.add(high, offset) + axis.min = other.min * ratio + axis.max = other.max * ratio - // Draw a major line - if is-major and (kind == 1 or kind == 3) { - draw.line(start, end, stroke: style.grid.stroke) - } - // Draw a minor line - if not is-major and kind >= 2 { - draw.line(start, end, stroke: style.minor-grid.stroke) - } + changed = true } - } -} - -// Place a list of tick marks and labels along a path -#let place-ticks-on-line(ticks, start, stop, style, flip: false, is-mirror: false) = { - let dir = vector.sub(stop, start) - let norm = vector.norm((-dir.at(1), dir.at(0), dir.at(2, default: 0))) - let def(v, d) = { - return if v == none or v == auto {d} else {v} - } - - let show-label = style.tick.label.show - if show-label == auto { - show-label = not is-mirror - } - - for (distance, label, is-major) in ticks { - let offset = style.tick.offset - let length = if is-major { style.tick.length } else { style.tick.minor-length } - if flip { - offset *= -1 - length *= -1 - } - - let pt = vector.lerp(start, stop, distance) - let a = vector.add(pt, vector.scale(norm, offset)) - let b = vector.add(a, vector.scale(norm, length)) - - draw.line(a, b, stroke: style.tick.stroke) - - if show-label and label != none { - let offset = style.tick.label.offset - if flip { - offset *= -1 - length *= -1 - } - - let c = vector.sub(if length <= 0 { b } else { a }, - vector.scale(norm, offset)) - - let angle = def(style.tick.label.angle, 0deg) - let anchor = def(style.tick.label.anchor, "center") - - draw.content(c, [#label], angle: angle, anchor: anchor) + if changed { + axis-dict.at(name) = axis } } -} - -// Draw up to four axes in an "scientific" style at origin (0, 0) -// -// - size (array): Size (width, height) -// - left (axis): Left (y) axis -// - bottom (axis): Bottom (x) axis -// - right (axis): Right axis -// - top (axis): Top axis -// - name (string): Object name -// - draw-unset (bool): Draw axes that are set to `none` -// - ..style (any): Style -#let scientific(size: (1, 1), - left: none, - right: auto, - bottom: none, - top: auto, - draw-unset: true, - name: none, - ..style) = { - import draw: * - if right == auto { - if left != none { - right = left; right.is-mirror = true - } else { - right = none - } - } - if top == auto { - if bottom != none { - top = bottom; top.is-mirror = true - } else { - top = none - } + for (name, axis) in axis-dict { + axis-dict.at(name) = prepare-axis(ctx, axis, name) } - group(name: name, ctx => { - let (w, h) = size - anchor("origin", (0, 0)) - - let style = style.named() - style = styles.resolve(ctx.style, merge: style, root: "axes", - base: default-style-scientific) - style = _prepare-style(ctx, style) - - // Compute ticks - let x-ticks = compute-ticks(bottom, style) - let y-ticks = compute-ticks(left, style) - let x2-ticks = compute-ticks(top, style) - let y2-ticks = compute-ticks(right, style) - - // Draw frame - if style.fill != none { - on-layer(style.background-layer, { - rect((0,0), (w,h), fill: style.fill, stroke: none) - }) - } - - // Draw grid - group(name: "grid", ctx => { - let axes = ( - ("bottom", (0,0), (0,h), (+w,0), x-ticks, bottom), - ("top", (0,h), (0,0), (+w,0), x2-ticks, top), - ("left", (0,0), (w,0), (0,+h), y-ticks, left), - ("right", (w,0), (0,0), (0,+h), y2-ticks, right), - ) - for (name, start, end, direction, ticks, axis) in axes { - if axis == none { continue } - - let style = _get-axis-style(ctx, style, name) - let is-mirror = axis.at("is-mirror", default: false) - - if not is-mirror { - on-layer(style.grid-layer, { - draw-grid-lines(ctx, axis, ticks, start, end, direction, style) - }) - } - } - }) - - // Draw axes - group(name: "axes", { - let axes = ( - ("bottom", (0, 0), (w, 0), (0, -1), false, x-ticks, bottom,), - ("top", (0, h), (w, h), (0, +1), true, x2-ticks, top,), - ("left", (0, 0), (0, h), (-1, 0), true, y-ticks, left,), - ("right", (w, 0), (w, h), (+1, 0), false, y2-ticks, right,) - ) - let label-placement = ( - bottom: ("south", "north", 0deg), - top: ("north", "south", 0deg), - left: ("west", "south", 90deg), - right: ("east", "north", 90deg), - ) - - for (name, start, end, outsides, flip, ticks, axis) in axes { - let style = _get-axis-style(ctx, style, name) - let is-mirror = axis == none or axis.at("is-mirror", default: false) - let is-horizontal = name in ("bottom", "top") - - if style.padding != 0 { - let padding = vector.scale(outsides, style.padding) - start = vector.add(start, padding) - end = vector.add(end, padding) - } - - let (data-start, data-end) = _inset-axis-points(ctx, style, axis, start, end) - - let path = _draw-axis-line(start, end, axis, is-horizontal, style) - on-layer(style.axis-layer, { - group(name: "axis", { - if draw-unset or axis != none { - path; - place-ticks-on-line(ticks, data-start, data-end, style, flip: flip, is-mirror: is-mirror) - } - }) - - if axis != none and axis.label != none and not is-mirror { - let offset = vector.scale(outsides, style.label.offset) - let (group-anchor, content-anchor, angle) = label-placement.at(name) - - if style.label.anchor != auto { - content-anchor = style.label.anchor - } - if style.label.angle != auto { - angle = style.label.angle - } - - content((rel: offset, to: "axis." + group-anchor), - [#axis.label], - angle: angle, - anchor: content-anchor) - } - }) - } - }) - }) + return axis-dict } -// Draw two axes in a "school book" style -// -// - x-axis (axis): X axis -// - y-axis (axis): Y axis -// - size (array): Size (width, height) -// - x-position (number): X Axis position -// - y-position (number): Y Axis position -// - name (string): Object name -// - ..style (any): Style -#let school-book(x-axis, y-axis, - size: (1, 1), - x-position: 0, - y-position: 0, - name: none, - ..style) = { - import draw: * - - group(name: name, ctx => { - let (w, h) = size - anchor("origin", (0, 0)) - - let style = style.named() - style = styles.resolve( - ctx.style, - merge: style, - root: "axes", - base: default-style-schoolbook) - style = _prepare-style(ctx, style) - - let x-position = calc.min(calc.max(y-axis.min, x-position), y-axis.max) - let y-position = calc.min(calc.max(x-axis.min, y-position), x-axis.max) - let x-y = value-on-axis(y-axis, x-position) * h - let y-x = value-on-axis(x-axis, y-position) * w - let shared-zero = style.shared-zero != false and x-position == 0 and y-position == 0 - let x-ticks = compute-ticks(x-axis, style, add-zero: not shared-zero) - let y-ticks = compute-ticks(y-axis, style, add-zero: not shared-zero) - // Draw grid - group(name: "grid", ctx => { - let axes = ( - ("x", (0,0), (0,h), (+w,0), x-ticks, x-axis), - ("y", (0,0), (w,0), (0,+h), y-ticks, y-axis), - ) +#import "axes/viewport.typ": transform-vec, axis-viewport - for (name, start, end, direction, ticks, axis) in axes { - if axis == none { continue } - - let style = _get-axis-style(ctx, style, name) - on-layer(style.grid-layer, { - draw-grid-lines(ctx, axis, ticks, start, end, direction, style) - }) - } - }) - - // Draw axes - group(name: "axes", { - let axes = ( - ("x", (0, x-y), (w, x-y), (1, 0), false, x-ticks, x-axis), - ("y", (y-x, 0), (y-x, h), (0, 1), true, y-ticks, y-axis), - ) - let label-pos = ( - x: ("north", (0,-1)), - y: ("east", (-1,0)), - ) - - on-layer(style.axis-layer, { - for (name, start, end, dir, flip, ticks, axis) in axes { - let style = _get-axis-style(ctx, style, name) - - let pad = style.padding - let overshoot = style.overshoot - let vstart = vector.sub(start, vector.scale(dir, pad)) - let vend = vector.add(end, vector.scale(dir, pad + overshoot)) - let is-horizontal = name == "x" - - let (data-start, data-end) = _inset-axis-points(ctx, style, axis, start, end) - group(name: "axis", { - _draw-axis-line(vstart, vend, axis, is-horizontal, style) - place-ticks-on-line(ticks, data-start, data-end, style, flip: flip) - }) - - if axis.label != none { - let (content-anchor, offset-dir) = label-pos.at(name) - - let angle = if style.label.angle not in (none, auto) { - style.label.angle - } else { 0deg } - if style.label.anchor not in (none, auto) { - content-anchor = style.label.anchor - } - - let offset = vector.scale(offset-dir, style.label.offset) - content((rel: offset, to: vend), - [#axis.label], - angle: angle, - anchor: content-anchor) - } - } - - if shared-zero { - let pt = (rel: (-style.tick.label.offset, -style.tick.label.offset), - to: (y-x, x-y)) - let zero = if type(style.shared-zero) == typst-content { - style.shared-zero - } else { - $0$ - } - content(pt, zero, anchor: "north-east") - } - }) - }) - }) -} +#import "axes/grid.typ": draw-grid-lines +#import "axes/ticks.typ": place-ticks-on-line +#import "axes/presets/scientific.typ": scientific +#import "axes/presets/school-book.typ": school-book \ No newline at end of file diff --git a/src/axes/draw.typ b/src/axes/draw.typ new file mode 100644 index 0000000..da03a5a --- /dev/null +++ b/src/axes/draw.typ @@ -0,0 +1,64 @@ +#import "/src/cetz.typ": draw, util, vector + +#let _inset-axis-points(ctx, style, axis, start, end) = { + if axis == none { return (start, end) } + + let (low, high) = axis.inset.map(v => util.resolve-number(ctx, v)) + + let is-horizontal = start.at(1) == end.at(1) + if is-horizontal { + start = vector.add(start, (low, 0)) + end = vector.sub(end, (high, 0)) + } else { + start = vector.add(start, (0, low)) + end = vector.sub(end, (0, high)) + } + return (start, end) +} + +#let _draw-axis-line(start, end, axis, is-horizontal, style) = { + let enabled = if axis != none and axis.show-break { + axis.min > 0 or axis.max < 0 + } else { false } + + if enabled { + let size = if is-horizontal { + (style.break-point.width, 0) + } else { + (0, style.break-point.width, 0) + } + + let up = if is-horizontal { + (0, style.break-point.length) + } else { + (style.break-point.length, 0) + } + + let add-break(is-end) = { + let a = () + let b = (rel: vector.scale(size, .3), update: false) + let c = (rel: vector.add(vector.scale(size, .4), vector.scale(up, -1)), update: false) + let d = (rel: vector.add(vector.scale(size, .6), vector.scale(up, +1)), update: false) + let e = (rel: vector.scale(size, .7), update: false) + let f = (rel: size) + + let mark = if is-end { + style.at("mark", default: none) + } + draw.line(a, b, c, d, e, f, stroke: style.stroke, mark: mark) + } + + draw.merge-path({ + draw.move-to(start) + if axis.min > 0 { + add-break(false) + draw.line((rel: size, to: start), end, mark: style.at("mark", default: none)) + } else if axis.max < 0 { + draw.line(start, (rel: vector.scale(size, -1), to: end)) + add-break(true) + } + }, stroke: style.stroke) + } else { + draw.line(start, end, stroke: style.stroke, mark: style.at("mark", default: none)) + } +} diff --git a/src/axes/grid.typ b/src/axes/grid.typ new file mode 100644 index 0000000..c4485cf --- /dev/null +++ b/src/axes/grid.typ @@ -0,0 +1,45 @@ +#import "/src/cetz.typ": util, vector, draw + +#let _get-grid-type(axis) = { + let grid = axis.ticks.at("grid", default: false) + if grid == "major" or grid == true { return 1 } + if grid == "minor" { return 2 } + if grid == "both" { return 3 } + return 0 +} + +// Draw grid lines for the ticks of an axis +// +// - cxt (context): +// - axis (dictionary): The axis +// - ticks (array): The computed ticks +// - low (vector): Start position of a grid-line at tick 0 +// - high (vector): End position of a grid-line at tick 0 +// - dir (vector): Normalized grid direction vector along the grid axis +// - style (style): Axis style +#let draw-grid-lines(ctx, axis, ticks, low, high, dir, style) = { + let offset = (0,0) + if axis.inset != none { + let (inset-low, inset-high) = axis.inset.map(v => util.resolve-number(ctx, v)) + offset = vector.scale(vector.norm(dir), inset-low) + dir = vector.sub(dir, vector.scale(vector.norm(dir), inset-low + inset-high)) + } + + let kind = _get-grid-type(axis) + if kind > 0 { + for (distance, label, is-major) in ticks { + let offset = vector.add(vector.scale(dir, distance), offset) + let start = vector.add(low, offset) + let end = vector.add(high, offset) + + // Draw a major line + if is-major and (kind == 1 or kind == 3) { + draw.line(start, end, stroke: style.grid.stroke) + } + // Draw a minor line + if not is-major and kind >= 2 { + draw.line(start, end, stroke: style.minor-grid.stroke) + } + } + } +} \ No newline at end of file diff --git a/src/axes/preset.typ b/src/axes/preset.typ new file mode 100644 index 0000000..7c8f06c --- /dev/null +++ b/src/axes/preset.typ @@ -0,0 +1,2 @@ +#import "presets/scientific.typ" +#import "presets/school-book.typ": school-book \ No newline at end of file diff --git a/src/axes/presets/school-book.typ b/src/axes/presets/school-book.typ new file mode 100644 index 0000000..d6f3105 --- /dev/null +++ b/src/axes/presets/school-book.typ @@ -0,0 +1,139 @@ +#import "/src/cetz.typ": util, draw, styles, vector +#import "../style.typ": default-style, _prepare-style, _get-axis-style +#import "../draw.typ": _inset-axis-points, _draw-axis-line +#import "../grid.typ": draw-grid-lines +#import "../ticks.typ": * + +#let default-style-schoolbook = util.merge-dictionary(default-style, ( + x: (stroke: auto, fill: none, mark: (start: none, end: "straight"), + tick: (label: (anchor: "north"))), + y: (stroke: auto, fill: none, mark: (start: none, end: "straight"), + tick: (label: (anchor: "east"))), + label: (offset: .1cm), + origin: (label: (offset: .05cm)), + padding: .1cm, // Axis padding on both sides outsides the plotting area + overshoot: .5cm, // Axis end "overshoot" out of the plotting area + tick: ( + offset: -50%, + minor-offset: -50%, + length: .2cm, + minor-length: 70%, + ), + shared-zero: $0$, // Show zero tick label at (0, 0) +)) + +// Draw two axes in a "school book" style +// +// - x-axis (axis): X axis +// - y-axis (axis): Y axis +// - size (array): Size (width, height) +// - x-position (number): X Axis position +// - y-position (number): Y Axis position +// - name (string): Object name +// - ..style (any): Style +#let school-book(x-axis, y-axis, + size: (1, 1), + x-position: 0, + y-position: 0, + name: none, + ..style) = { + import draw: * + + group(name: name, ctx => { + let (w, h) = size + anchor("origin", (0, 0)) + + let style = style.named() + style = styles.resolve( + ctx.style, + merge: style, + root: "axes", + base: default-style-schoolbook) + style = _prepare-style(ctx, style) + + let x-position = calc.min(calc.max(y-axis.min, x-position), y-axis.max) + let y-position = calc.min(calc.max(x-axis.min, y-position), x-axis.max) + let x-y = value-on-axis(y-axis, x-position) * h + let y-x = value-on-axis(x-axis, y-position) * w + + let shared-zero = style.shared-zero != false and x-position == 0 and y-position == 0 + + let x-ticks = compute-ticks(x-axis, style, add-zero: not shared-zero) + let y-ticks = compute-ticks(y-axis, style, add-zero: not shared-zero) + + // Draw grid + group(name: "grid", ctx => { + let axes = ( + ("x", (0,0), (0,h), (+w,0), x-ticks, x-axis), + ("y", (0,0), (w,0), (0,+h), y-ticks, y-axis), + ) + + for (name, start, end, direction, ticks, axis) in axes { + if axis == none { continue } + + let style = _get-axis-style(ctx, style, name) + on-layer(style.grid-layer, { + draw-grid-lines(ctx, axis, ticks, start, end, direction, style) + }) + } + }) + + // Draw axes + group(name: "axes", { + let axes = ( + ("x", (0, x-y), (w, x-y), (1, 0), false, x-ticks, x-axis), + ("y", (y-x, 0), (y-x, h), (0, 1), true, y-ticks, y-axis), + ) + let label-pos = ( + x: ("north", (0,-1)), + y: ("east", (-1,0)), + ) + + on-layer(style.axis-layer, { + for (name, start, end, dir, flip, ticks, axis) in axes { + let style = _get-axis-style(ctx, style, name) + + let pad = style.padding + let overshoot = style.overshoot + let vstart = vector.sub(start, vector.scale(dir, pad)) + let vend = vector.add(end, vector.scale(dir, pad + overshoot)) + let is-horizontal = name == "x" + + let (data-start, data-end) = _inset-axis-points(ctx, style, axis, start, end) + group(name: "axis", { + _draw-axis-line(vstart, vend, axis, is-horizontal, style) + place-ticks-on-line(ticks, data-start, data-end, style, flip: flip) + }) + + if axis.label != none { + let (content-anchor, offset-dir) = label-pos.at(name) + + let angle = if style.label.angle not in (none, auto) { + style.label.angle + } else { 0deg } + if style.label.anchor not in (none, auto) { + content-anchor = style.label.anchor + } + + let offset = vector.scale(offset-dir, style.label.offset) + content((rel: offset, to: vend), + [#axis.label], + angle: angle, + anchor: content-anchor) + } + } + + if shared-zero { + let pt = (rel: (-style.tick.label.offset, -style.tick.label.offset), + to: (y-x, x-y)) + let zero = if type(style.shared-zero) == content { + style.shared-zero + } else { + $0$ + } + content(pt, zero, anchor: "north-east") + } + }) + }) + }) +} diff --git a/src/axes/presets/scientific.typ b/src/axes/presets/scientific.typ new file mode 100644 index 0000000..b968017 --- /dev/null +++ b/src/axes/presets/scientific.typ @@ -0,0 +1,153 @@ +#import "/src/cetz.typ": util, draw, styles, vector +#import "../style.typ": default-style, _prepare-style, _get-axis-style +#import "../draw.typ": _inset-axis-points, _draw-axis-line +#import "../grid.typ": draw-grid-lines +#import "../ticks.typ": * + +// Default Scientific Style +#let default-style-scientific = util.merge-dictionary(default-style, ( + left: (tick: (label: (anchor: "east"))), + bottom: (tick: (label: (anchor: "north"))), + right: (tick: (label: (anchor: "west"))), + top: (tick: (label: (anchor: "south"))), + stroke: (cap: "square"), + padding: 0, +)) + +// Draw up to four axes in an "scientific" style at origin (0, 0) +// +// - size (array): Size (width, height) +// - left (axis): Left (y) axis +// - bottom (axis): Bottom (x) axis +// - right (axis): Right axis +// - top (axis): Top axis +// - name (string): Object name +// - draw-unset (bool): Draw axes that are set to `none` +// - ..style (any): Style +#let scientific(size: (1, 1), + left: none, + right: auto, + bottom: none, + top: auto, + draw-unset: true, + name: none, + ..style) = { + import draw: * + + if right == auto { + if left != none { + right = left; right.is-mirror = true + } else { + right = none + } + } + if top == auto { + if bottom != none { + top = bottom; top.is-mirror = true + } else { + top = none + } + } + + group(name: name, ctx => { + let (w, h) = size + anchor("origin", (0, 0)) + + let style = style.named() + style = styles.resolve(ctx.style, merge: style, root: "axes", + base: default-style-scientific) + style = _prepare-style(ctx, style) + + // Compute ticks + let x-ticks = compute-ticks(bottom, style) + let y-ticks = compute-ticks(left, style) + let x2-ticks = compute-ticks(top, style) + let y2-ticks = compute-ticks(right, style) + + // Draw frame + if style.fill != none { + on-layer(style.background-layer, { + rect((0,0), (w,h), fill: style.fill, stroke: none) + }) + } + + // Draw grid + group(name: "grid", ctx => { + let axes = ( + ("bottom", (0,0), (0,h), (+w,0), x-ticks, bottom), + ("top", (0,h), (0,0), (+w,0), x2-ticks, top), + ("left", (0,0), (w,0), (0,+h), y-ticks, left), + ("right", (w,0), (0,0), (0,+h), y2-ticks, right), + ) + for (name, start, end, direction, ticks, axis) in axes { + if axis == none { continue } + + let style = _get-axis-style(ctx, style, name) + let is-mirror = axis.at("is-mirror", default: false) + + if not is-mirror { + on-layer(style.grid-layer, { + draw-grid-lines(ctx, axis, ticks, start, end, direction, style) + }) + } + } + }) + + // Draw axes + group(name: "axes", { + let axes = ( + ("bottom", (0, 0), (w, 0), (0, -1), false, x-ticks, bottom,), + ("top", (0, h), (w, h), (0, +1), true, x2-ticks, top,), + ("left", (0, 0), (0, h), (-1, 0), true, y-ticks, left,), + ("right", (w, 0), (w, h), (+1, 0), false, y2-ticks, right,) + ) + let label-placement = ( + bottom: ("south", "north", 0deg), + top: ("north", "south", 0deg), + left: ("west", "south", 90deg), + right: ("east", "north", 90deg), + ) + + for (name, start, end, outsides, flip, ticks, axis) in axes { + let style = _get-axis-style(ctx, style, name) + let is-mirror = axis == none or axis.at("is-mirror", default: false) + let is-horizontal = name in ("bottom", "top") + + if style.padding != 0 { + let padding = vector.scale(outsides, style.padding) + start = vector.add(start, padding) + end = vector.add(end, padding) + } + + let (data-start, data-end) = _inset-axis-points(ctx, style, axis, start, end) + + let path = _draw-axis-line(start, end, axis, is-horizontal, style) + on-layer(style.axis-layer, { + group(name: "axis", { + if draw-unset or axis != none { + path; + place-ticks-on-line(ticks, data-start, data-end, style, flip: flip, is-mirror: is-mirror) + } + }) + + if axis != none and axis.label != none and not is-mirror { + let offset = vector.scale(outsides, style.label.offset) + let (group-anchor, content-anchor, angle) = label-placement.at(name) + + if style.label.anchor != auto { + content-anchor = style.label.anchor + } + if style.label.angle != auto { + angle = style.label.angle + } + + content((rel: offset, to: "axis." + group-anchor), + [#axis.label], + angle: angle, + anchor: content-anchor) + } + }) + } + }) + }) +} diff --git a/src/axes/style.typ b/src/axes/style.typ new file mode 100644 index 0000000..2963bed --- /dev/null +++ b/src/axes/style.typ @@ -0,0 +1,117 @@ +#import "/src/cetz.typ": util, styles + +/// Default axis style +/// +/// #show-parameter-block("tick-limit", "int", default: 100, [Upper major tick limit.]) +/// #show-parameter-block("minor-tick-limit", "int", default: 1000, [Upper minor tick limit.]) +/// #show-parameter-block("auto-tick-factors", "array", [List of tick factors used for automatic tick step determination.]) +/// #show-parameter-block("auto-tick-count", "int", [Number of ticks to generate by default.]) +/// #show-parameter-block("stroke", "stroke", [Axis stroke style.]) +/// #show-parameter-block("label.offset", "number", [Distance to move axis labels away from the axis.]) +/// #show-parameter-block("label.anchor", "anchor", [Anchor of the axis label to use for it's placement.]) +/// #show-parameter-block("label.angle", "angle", [Angle of the axis label.]) +/// #show-parameter-block("axis-layer", "float", [Layer to draw axes on (see @@on-layer() )]) +/// #show-parameter-block("grid-layer", "float", [Layer to draw the grid on (see @@on-layer() )]) +/// #show-parameter-block("background-layer", "float", [Layer to draw the background on (see @@on-layer() )]) +/// #show-parameter-block("padding", "number", [Extra distance between axes and plotting area. For schoolbook axes, this is the length of how much axes grow out of the plotting area.]) +/// #show-parameter-block("overshoot", "number", [School-book style axes only: Extra length to add to the end (right, top) of axes.]) +/// #show-parameter-block("tick.stroke", "stroke", [Major tick stroke style.]) +/// #show-parameter-block("tick.minor-stroke", "stroke", [Minor tick stroke style.]) +/// #show-parameter-block("tick.offset", ("number", "ratio"), [Major tick offset along the tick's direction, can be relative to the length.]) +/// #show-parameter-block("tick.minor-offset", ("number", "ratio"), [Minor tick offset along the tick's direction, can be relative to the length.]) +/// #show-parameter-block("tick.length", ("number"), [Major tick length.]) +/// #show-parameter-block("tick.minor-length", ("number", "ratio"), [Minor tick length, can be relative to the major tick length.]) +/// #show-parameter-block("tick.label.offset", ("number"), [Major tick label offset away from the tick.]) +/// #show-parameter-block("tick.label.angle", ("angle"), [Major tick label angle.]) +/// #show-parameter-block("tick.label.anchor", ("anchor"), [Anchor of major tick labels used for positioning.]) +/// #show-parameter-block("tick.label.show", ("auto", "bool"), default: auto, [Set visibility of tick labels. A value of `auto` shows tick labels for all but mirrored axes.]) +/// #show-parameter-block("grid.stroke", "stroke", [Major grid line stroke style.]) +/// #show-parameter-block("break-point.width", "number", [Axis break width along the axis.]) +/// #show-parameter-block("break-point.length", "number", [Axis break length.]) +/// #show-parameter-block("minor-grid.stroke", "stroke", [Minor grid line stroke style.]) +/// #show-parameter-block("shared-zero", ("bool", "content"), default: "$0$", [School-book style axes only: Content to display at the plots origin (0,0). If set to `false`, nothing is shown. Having this set, suppresses auto-generated ticks for $0$!]) +#let default-style = ( + tick-limit: 100, + minor-tick-limit: 1000, + auto-tick-factors: (1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10), // Tick factor to try + auto-tick-count: 11, // Number of ticks the plot tries to place + fill: none, + stroke: auto, + label: ( + offset: .2cm, // Axis label offset + anchor: auto, // Axis label anchor + angle: auto, // Axis label angle + ), + axis-layer: 0, + grid-layer: 0, + background-layer: 0, + padding: 0, + tick: ( + fill: none, + stroke: black + 1pt, + minor-stroke: black + .5pt, + offset: 0, + minor-offset: 0, + length: .1cm, // Tick length: Number + minor-length: 70%, // Minor tick length: Number, Ratio + label: ( + offset: .15cm, // Tick label offset + angle: 0deg, // Tick label angle + anchor: auto, // Tick label anchor + "show": auto, // Show tick labels for axes in use + ) + ), + break-point: ( + width: .75cm, + length: .15cm, + ), + grid: ( + stroke: (paint: gray.lighten(50%), thickness: 1pt), + ), + minor-grid: ( + stroke: (paint: gray.lighten(50%), thickness: .5pt), + ), +) + + +#let _prepare-style(ctx, style) = { + if type(style) != dictionary { return style } + + let res = util.resolve-number.with(ctx) + let rel-to(v, to) = { + if type(v) == ratio { + return v * to / 100% + } else { + return res(v) + } + } + + style.tick.length = res(style.tick.length) + style.tick.offset = rel-to(style.tick.offset, style.tick.length) + style.tick.minor-length = rel-to(style.tick.minor-length, style.tick.length) + style.tick.minor-offset = rel-to(style.tick.minor-offset, style.tick.minor-length) + style.tick.label.offset = res(style.tick.label.offset) + + // Break points + style.break-point.width = res(style.break-point.width) + style.break-point.length = res(style.break-point.length) + + // Padding + style.padding = res(style.padding) + + if "overshoot" in style { + style.overshoot = res(style.overshoot) + } + + return style +} + +#let _get-axis-style(ctx, style, name) = { + if not name in style { + return style + } + + style = styles.resolve(style, merge: style.at(name)) + return _prepare-style(ctx, style) +} + diff --git a/src/axes/ticks.typ b/src/axes/ticks.typ new file mode 100644 index 0000000..b9928e3 --- /dev/null +++ b/src/axes/ticks.typ @@ -0,0 +1,251 @@ +#import "/src/cetz.typ": vector, util, draw + +// Format a tick value +#let format-tick-value(value, tic-options) = { + // Without it we get negative zero in conversion + // to content! Typst has negative zero floats. + if value == 0 { value = 0 } + + let round(value, digits) = { + calc.round(value, digits: digits) + } + + let format-float(value, digits) = { + $#round(value, digits)$ + } + + let format-sci(value, digits) = { + let exponent = if value != 0 { + calc.floor(calc.log(calc.abs(value), base: 10)) + } else { + 0 + } + + let ee = calc.pow(10, calc.abs(exponent + 1)) + if exponent > 0 { + value = value / ee * 10 + } else if exponent < 0 { + value = value * ee * 10 + } + + value = round(value, digits) + if exponent <= -1 or exponent >= 1 { + return $#value times 10^#exponent$ + } + return $#value$ + } + + if type(value) != content { + let format = tic-options.at("format", default: "float") + if format == none { + value = [] + } else if type(format) == content { + value = format + } else if type(format) == function { + value = (format)(value) + } else if format == "sci" { + value = format-sci(value, tic-options.at("decimals", default: 2)) + } else { + value = format-float(value, tic-options.at("decimals", default: 2)) + } + } else { + value = str(value) + } + + if tic-options.at("unit", default: none) != none { + value += tic-options.unit + } + return value +} + +// Get value on axis [0, 1] +// +// - axis (axis): Axis +// - v (number): Value +// -> float +#let value-on-axis(axis, v) = { + if v == none { return } + let (min, max) = (axis.min, axis.max) + let dt = max - min; if dt == 0 { dt = 1 } + + return (v - min) / dt +} + +// Compute list of linear ticks for axis +// +// - axis (axis): Axis +#let compute-linear-ticks(axis, style, add-zero: true) = { + let (min, max) = (axis.min, axis.max) + let dt = max - min; if (dt == 0) { dt = 1 } + let ticks = axis.ticks + let ferr = util.float-epsilon + let tick-limit = style.tick-limit + let minor-tick-limit = style.minor-tick-limit + + let l = () + if ticks != none { + let major-tick-values = () + if "step" in ticks and ticks.step != none { + assert(ticks.step >= 0, + message: "Axis tick step must be positive and non 0.") + if axis.min > axis.max { ticks.step *= -1 } + + let s = 1 / ticks.step + + let num-ticks = int(max * s + 1.5) - int(min * s) + assert(num-ticks <= tick-limit, + message: "Number of major ticks exceeds limit " + str(tick-limit)) + + let n = range(int(min * s), int(max * s + 1.5)) + for t in n { + let v = (t / s - min) / dt + if t / s == 0 and not add-zero { continue } + + if v >= 0 - ferr and v <= 1 + ferr { + l.push((v, format-tick-value(t / s, ticks), true)) + major-tick-values.push(v) + } + } + } + + if "minor-step" in ticks and ticks.minor-step != none { + assert(ticks.minor-step >= 0, + message: "Axis minor tick step must be positive") + if axis.min > axis.max { ticks.minor-step *= -1 } + + let s = 1 / ticks.minor-step + + let num-ticks = int(max * s + 1.5) - int(min * s) + assert(num-ticks <= minor-tick-limit, + message: "Number of minor ticks exceeds limit " + str(minor-tick-limit)) + + let n = range(int(min * s), int(max * s + 1.5)) + for t in n { + let v = (t / s - min) / dt + if v in major-tick-values { + // Prefer major ticks over minor ticks + continue + } + + if v != none and v >= 0 and v <= 1 + ferr { + l.push((v, none, false)) + } + } + } + + } + + return l +} + +// Get list of fixed axis ticks +// +// - axis (axis): Axis object +#let fixed-ticks(axis) = { + let l = () + if "list" in axis.ticks { + for t in axis.ticks.list { + let (v, label) = (none, none) + if type(t) in (float, int) { + v = t + label = format-tick-value(t, axis.ticks) + } else { + (v, label) = t + } + + v = value-on-axis(axis, v) + if v != none and v >= 0 and v <= 1 { + l.push((v, label, true)) + } + } + } + return l +} + +// Compute list of axis ticks +// +// A tick triple has the format: +// (rel-value: float, label: content, major: bool) +// +// - axis (axis): Axis object +#let compute-ticks(axis, style, add-zero: true) = { + let find-max-n-ticks(axis, n: 11) = { + let dt = calc.abs(axis.max - axis.min) + let scale = calc.floor(calc.log(dt, base: 10) - 1) + if scale > 5 or scale < -5 {return none} + + let (step, best) = (none, 0) + for s in style.auto-tick-factors { + s = s * calc.pow(10, scale) + + let divs = calc.abs(dt / s) + if divs >= best and divs <= n { + step = s + best = divs + } + } + return step + } + + if axis == none or axis.ticks == none { return () } + if axis.ticks.step == auto { + axis.ticks.step = find-max-n-ticks(axis, n: style.auto-tick-count) + } + if axis.ticks.minor-step == auto { + axis.ticks.minor-step = if axis.ticks.step != none { + axis.ticks.step / 5 + } else { + none + } + } + + let ticks = compute-linear-ticks(axis, style, add-zero: add-zero) + ticks += fixed-ticks(axis) + return ticks +} + +// Place a list of tick marks and labels along a path +#let place-ticks-on-line(ticks, start, stop, style, flip: false, is-mirror: false) = { + let dir = vector.sub(stop, start) + let norm = vector.norm((-dir.at(1), dir.at(0), dir.at(2, default: 0))) + + let def(v, d) = { + return if v == none or v == auto {d} else {v} + } + + let show-label = style.tick.label.show + if show-label == auto { + show-label = not is-mirror + } + + for (distance, label, is-major) in ticks { + let offset = style.tick.offset + let length = if is-major { style.tick.length } else { style.tick.minor-length } + if flip { + offset *= -1 + length *= -1 + } + + let pt = vector.lerp(start, stop, distance) + let a = vector.add(pt, vector.scale(norm, offset)) + let b = vector.add(a, vector.scale(norm, length)) + + draw.line(a, b, stroke: style.tick.stroke) + + if show-label and label != none { + let offset = style.tick.label.offset + if flip { + offset *= -1 + length *= -1 + } + + let c = vector.sub(if length <= 0 { b } else { a }, + vector.scale(norm, offset)) + + let angle = def(style.tick.label.angle, 0deg) + let anchor = def(style.tick.label.anchor, "center") + + draw.content(c, [#label], angle: angle, anchor: anchor) + } + } +} \ No newline at end of file diff --git a/src/axes/util.typ b/src/axes/util.typ new file mode 100644 index 0000000..e69de29 diff --git a/src/axes/viewport.typ b/src/axes/viewport.typ new file mode 100644 index 0000000..034fa41 --- /dev/null +++ b/src/axes/viewport.typ @@ -0,0 +1,78 @@ +#import "/src/cetz.typ": draw, drawable, matrix, process, util + +// Transform a single vector along a x, y and z axis +// +// - size (vector): Coordinate system size +// - x-axis (axis): X axis +// - y-axis (axis): Y axis +// - z-axis (axis): Z axis +// - vec (vector): Input vector to transform +// -> vector +#let transform-vec(size, x-axis, y-axis, z-axis, vec) = { + let (ox, oy, ..) = (0, 0, 0) + ox += x-axis.inset.at(0) + oy += y-axis.inset.at(0) + + let (sx, sy) = size + sx -= x-axis.inset.sum() + sy -= y-axis.inset.sum() + + let x-range = x-axis.max - x-axis.min + let y-range = y-axis.max - y-axis.min + let z-range = 0 //z-axis.max - z-axis.min + + let fx = sx / x-range + let fy = sy / y-range + let fz = 0 //sz / z-range + + let x-low = calc.min(x-axis.min, x-axis.max) + let x-high = calc.max(x-axis.min, x-axis.max) + let y-low = calc.min(y-axis.min, y-axis.max) + let y-high = calc.max(y-axis.min, y-axis.max) + //let z-low = calc.min(z-axis.min, z-axis.max) + //let z-hihg = calc.max(z-axis.min, z-axis.max) + + let (x, y, ..) = vec + + return ( + (x - x-axis.min) * fx + ox, + (y - y-axis.min) * fy + oy, + 0) //(z - z-axis.min) * fz + oz) +} + +// Draw inside viewport coordinates of two axes +// +// - size (vector): Axis canvas size (relative to origin) +// - x (axis): Horizontal axis +// - y (axis): Vertical axis +// - z (axis): Z axis +// - name (string,none): Group name +#let axis-viewport(size, x, y, z, body, name: none) = { + draw.group(name: name, (ctx => { + let transform = ctx.transform + + ctx.transform = matrix.ident() + let (ctx, drawables, bounds) = process.many(ctx, util.resolve-body(ctx, body)) + + ctx.transform = transform + + drawables = drawables.map(d => { + if "segments" in d { + d.segments = d.segments.map(((kind, ..pts)) => { + (kind, ..pts.map(pt => { + transform-vec(size, x, y, none, pt) + })) + }) + } + if "pos" in d { + d.pos = transform-vec(size, x, y, none, d.pos) + } + return d + }) + + return ( + ctx: ctx, + drawables: drawable.apply-transform(ctx.transform, drawables) + ) + },)) +} \ No newline at end of file diff --git a/src/plot.typ b/src/plot.typ index 8a062dc..3d66d62 100644 --- a/src/plot.typ +++ b/src/plot.typ @@ -303,7 +303,7 @@ } // Set axis options - axis-dict = plot-util.setup-axes(ctx, axis-dict, options.named(), size) + axis-dict = axes.setup-axes(ctx, axis-dict, options.named(), size) // Prepare styles for i in range(data.len()) { diff --git a/src/plot/util.typ b/src/plot/util.typ index 13a9e59..dce18ac 100644 --- a/src/plot/util.typ +++ b/src/plot/util.typ @@ -268,102 +268,3 @@ return pts } -// Get the default axis orientation -// depending on the axis name -#let get-default-axis-horizontal(name) = { - return lower(name).starts-with("x") -} - -// Setup axes dictionary -// -// - axis-dict (dictionary): Existing axis dictionary -// - options (dictionary): Named arguments -// - plot-size (tuple): Plot width, height tuple -#let setup-axes(ctx, axis-dict, options, plot-size) = { - import "/src/axes.typ" - - // Get axis option for name - let get-axis-option(axis-name, name, default) = { - let v = options.at(axis-name + "-" + name, default: default) - if v == auto { default } else { v } - } - - for (name, axis) in axis-dict { - if not "ticks" in axis { axis.ticks = () } - axis.label = get-axis-option(name, "label", $#name$) - - // Configure axis bounds - axis.min = get-axis-option(name, "min", axis.min) - axis.max = get-axis-option(name, "max", axis.max) - - assert(axis.min not in (none, auto) and - axis.max not in (none, auto), - message: "Axis min and max must be set.") - if axis.min == axis.max { - axis.min -= 1; axis.max += 1 - } - - // Configure axis orientation - axis.horizontal = get-axis-option(name, "horizontal", - get-default-axis-horizontal(name)) - - // Configure ticks - axis.ticks.list = get-axis-option(name, "ticks", ()) - axis.ticks.step = get-axis-option(name, "tick-step", axis.ticks.step) - axis.ticks.minor-step = get-axis-option(name, "minor-tick-step", axis.ticks.minor-step) - axis.ticks.decimals = get-axis-option(name, "decimals", 2) - axis.ticks.unit = get-axis-option(name, "unit", []) - axis.ticks.format = get-axis-option(name, "format", axis.ticks.format) - - // Axis break - axis.show-break = get-axis-option(name, "break", false) - axis.inset = get-axis-option(name, "inset", (0, 0)) - - // Configure grid - axis.ticks.grid = get-axis-option(name, "grid", false) - - axis-dict.at(name) = axis - } - - // Set axis options round two, after setting - // axis bounds - for (name, axis) in axis-dict { - let changed = false - - // Configure axis aspect ratio - let equal-to = get-axis-option(name, "equal", none) - if equal-to != none { - assert.eq(type(equal-to), str, - message: "Expected axis name.") - assert(equal-to != name, - message: "Axis can not be equal to itself.") - - let other = axis-dict.at(equal-to, default: none) - assert(other != none, - message: "Other axis must exist.") - assert(other.horizontal != axis.horizontal, - message: "Equal axes must have opposing orientation.") - - let (w, h) = plot-size - let ratio = if other.horizontal { - h / w - } else { - w / h - } - axis.min = other.min * ratio - axis.max = other.max * ratio - - changed = true - } - - if changed { - axis-dict.at(name) = axis - } - } - - for (name, axis) in axis-dict { - axis-dict.at(name) = axes.prepare-axis(ctx, axis, name) - } - - return axis-dict -} From ee6e8d006b17398efbe294140945434d6fbfc688 Mon Sep 17 00:00:00 2001 From: James R Swift Date: Mon, 29 Jul 2024 14:16:48 +0100 Subject: [PATCH 2/9] remove `axes/util.typ` --- src/axes/util.typ | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/axes/util.typ diff --git a/src/axes/util.typ b/src/axes/util.typ deleted file mode 100644 index e69de29..0000000 From a76df80cac530bb879891157c74374e53ccd3518 Mon Sep 17 00:00:00 2001 From: James R Swift Date: Mon, 29 Jul 2024 18:18:36 +0100 Subject: [PATCH 3/9] Polar plot start --- src/axes.typ | 3 +- src/axes/preset.typ | 3 +- src/axes/presets/scientific-polar.typ | 154 ++++++++++++++++++++++++++ src/plot.typ | 11 +- tests/plot/polar/test.typ | 24 ++++ 5 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 src/axes/presets/scientific-polar.typ create mode 100644 tests/plot/polar/test.typ diff --git a/src/axes.typ b/src/axes.typ index 981ada7..478c3b0 100644 --- a/src/axes.typ +++ b/src/axes.typ @@ -152,5 +152,4 @@ #import "axes/grid.typ": draw-grid-lines #import "axes/ticks.typ": place-ticks-on-line -#import "axes/presets/scientific.typ": scientific -#import "axes/presets/school-book.typ": school-book \ No newline at end of file +#import "axes/preset.typ": scientific, scientific-polar, school-book \ No newline at end of file diff --git a/src/axes/preset.typ b/src/axes/preset.typ index 7c8f06c..9208ff2 100644 --- a/src/axes/preset.typ +++ b/src/axes/preset.typ @@ -1,2 +1,3 @@ #import "presets/scientific.typ" -#import "presets/school-book.typ": school-book \ No newline at end of file +#import "presets/school-book.typ": school-book +#import "presets/scientific-polar.typ": scientific-polar \ No newline at end of file diff --git a/src/axes/presets/scientific-polar.typ b/src/axes/presets/scientific-polar.typ new file mode 100644 index 0000000..7848f8f --- /dev/null +++ b/src/axes/presets/scientific-polar.typ @@ -0,0 +1,154 @@ +#import "/src/cetz.typ": util, draw, styles, vector +#import "../style.typ": _prepare-style, _get-axis-style +#import "../draw.typ": _inset-axis-points, _draw-axis-line +#import "../grid.typ": draw-grid-lines +#import "../ticks.typ": * +#import "scientific.typ": default-style-scientific + +// Default Scientific Style +#let default-style-scientific-polar = util.merge-dictionary(default-style-scientific, ( + left: (tick: (label: (anchor: "east"))), + bottom: (tick: (label: (anchor: "north"))), + right: (tick: (label: (anchor: "west"))), + top: (tick: (label: (anchor: "south"))), + stroke: (cap: "square"), + padding: 0, +)) + +// Draw up to four axes in an "scientific" style at origin (0, 0) +// +// - size (array): Size (width, height) +// - left (axis): Left (y) axis +// - bottom (axis): Bottom (x) axis +// - right (axis): Right axis +// - top (axis): Top axis +// - name (string): Object name +// - draw-unset (bool): Draw axes that are set to `none` +// - ..style (any): Style +#let scientific-polar(size: (1, 1), + left: none, + right: auto, + bottom: none, + top: auto, + draw-unset: true, + name: none, + ..style) = { + import draw: * + + if right == auto { + if left != none { + right = left; right.is-mirror = true + } else { + right = none + } + } + if top == auto { + if bottom != none { + top = bottom; top.is-mirror = true + } else { + top = none + } + } + + group(name: name, ctx => { + let (w, h) = size + anchor("origin", (0, 0)) + + let style = style.named() + style = styles.resolve(ctx.style, merge: style, root: "axes", + base: default-style-scientific) + style = _prepare-style(ctx, style) + + // Compute ticks + let x-ticks = compute-ticks(bottom, style) + let y-ticks = compute-ticks(left, style) + let x2-ticks = compute-ticks(top, style) + let y2-ticks = compute-ticks(right, style) + + // Draw frame + if style.fill != none { + on-layer(style.background-layer, { + rect((0,0), (w,h), fill: style.fill, stroke: none) + }) + } + + // Draw grid + group(name: "grid", ctx => { + let axes = ( + ("bottom", (0,0), (0,h), (+w,0), x-ticks, bottom), + ("top", (0,h), (0,0), (+w,0), x2-ticks, top), + ("left", (0,0), (w,0), (0,+h), y-ticks, left), + ("right", (w,0), (0,0), (0,+h), y2-ticks, right), + ) + for (name, start, end, direction, ticks, axis) in axes { + if axis == none { continue } + + let style = _get-axis-style(ctx, style, name) + let is-mirror = axis.at("is-mirror", default: false) + + if not is-mirror { + on-layer(style.grid-layer, { + draw-grid-lines(ctx, axis, ticks, start, end, direction, style) + }) + } + } + }) + + // Draw axes + group(name: "axes", { + let axes = ( + ("bottom", (0, 0), (w, 0), (0, -1), false, x-ticks, bottom,), + ("top", (0, h), (w, h), (0, +1), true, x2-ticks, top,), + ("left", (0, 0), (0, h), (-1, 0), true, y-ticks, left,), + ("right", (w, 0), (w, h), (+1, 0), false, y2-ticks, right,) + ) + let label-placement = ( + bottom: ("south", "north", 0deg), + top: ("north", "south", 0deg), + left: ("west", "south", 90deg), + right: ("east", "north", 90deg), + ) + + for (name, start, end, outsides, flip, ticks, axis) in axes { + let style = _get-axis-style(ctx, style, name) + let is-mirror = axis == none or axis.at("is-mirror", default: false) + let is-horizontal = name in ("bottom", "top") + + if style.padding != 0 { + let padding = vector.scale(outsides, style.padding) + start = vector.add(start, padding) + end = vector.add(end, padding) + } + + let (data-start, data-end) = _inset-axis-points(ctx, style, axis, start, end) + + let path = _draw-axis-line(start, end, axis, is-horizontal, style) + on-layer(style.axis-layer, { + group(name: "axis", { + if draw-unset or axis != none { + path; + place-ticks-on-line(ticks, data-start, data-end, style, flip: flip, is-mirror: is-mirror) + } + }) + + if axis != none and axis.label != none and not is-mirror { + let offset = vector.scale(outsides, style.label.offset) + let (group-anchor, content-anchor, angle) = label-placement.at(name) + + if style.label.anchor != auto { + content-anchor = style.label.anchor + } + if style.label.angle != auto { + angle = style.label.angle + } + + content((rel: offset, to: "axis." + group-anchor), + [#axis.label], + angle: angle, + anchor: content-anchor) + } + }) + } + }) + }) +} diff --git a/src/plot.typ b/src/plot.typ index 3d66d62..51c73b9 100644 --- a/src/plot.typ +++ b/src/plot.typ @@ -256,7 +256,7 @@ } else { data.push(cmd) } } - assert(axis-style in (none, "scientific", "scientific-auto", "school-book", "left"), + assert(axis-style in (none, "scientific", "scientific-auto", "scientific-polar","school-book", "left"), message: "Invalid plot style") // Create axes for data & annotations @@ -425,6 +425,15 @@ size: size, axis-dict.x, axis-dict.y,) + } else if axis-style == "scientific-polar" { + axes.scientific-polar( + size: size, + draw-unset: false, + bottom: axis-dict.at("x", default: none), + top: axis-dict.at("x2", default: auto), + left: axis-dict.at("y", default: none), + right: axis-dict.at("y2", default: auto), + ) } // Stroke + Mark data diff --git a/tests/plot/polar/test.typ b/tests/plot/polar/test.typ new file mode 100644 index 0000000..2c7451c --- /dev/null +++ b/tests/plot/polar/test.typ @@ -0,0 +1,24 @@ +#set page(width: auto, height: auto) +#import "/src/cetz.typ": * +#import "/src/lib.typ": * +#import "/tests/helper.typ": * + +#let line-data = ((-1,-1), (1,1),) + +#let data = (..(for x in range(-360, 360 + 1) { + ((x, calc.sin(x * 1deg)),) +})) + +/* Scientific Style */ +#test-case({ + plot.plot( + size: (5, 2), + axis-style: "scientific-polar", + x-tick-step: 180, + y-tick-step: 1, + x-grid: "major", + y-grid: "major", + { + plot.add(data) + }) +}) \ No newline at end of file From 630632bd5ce9e5665a91c0f67400a6bdfb53f320 Mon Sep 17 00:00:00 2001 From: James R Swift Date: Tue, 30 Jul 2024 17:04:24 +0100 Subject: [PATCH 4/9] Stepwise towards polar plots --- src/axes/presets/scientific-polar.typ | 147 +++++++++++++------------- src/plot.typ | 6 +- tests/plot/polar/test.typ | 19 ++-- 3 files changed, 87 insertions(+), 85 deletions(-) diff --git a/src/axes/presets/scientific-polar.typ b/src/axes/presets/scientific-polar.typ index 7848f8f..e79c4d0 100644 --- a/src/axes/presets/scientific-polar.typ +++ b/src/axes/presets/scientific-polar.typ @@ -15,40 +15,61 @@ padding: 0, )) -// Draw up to four axes in an "scientific" style at origin (0, 0) -// -// - size (array): Size (width, height) -// - left (axis): Left (y) axis -// - bottom (axis): Bottom (x) axis -// - right (axis): Right axis -// - top (axis): Top axis -// - name (string): Object name -// - draw-unset (bool): Draw axes that are set to `none` -// - ..style (any): Style -#let scientific-polar(size: (1, 1), - left: none, - right: auto, - bottom: none, - top: auto, - draw-unset: true, - name: none, - ..style) = { +#let _get-grid-type(axis) = { + let grid = axis.ticks.at("grid", default: false) + if grid == "major" or grid == true { return 1 } + if grid == "minor" { return 2 } + if grid == "both" { return 3 } + return 0 +} + +#let _draw-polar-grid-lines(ctx, name, axis, ticks, radius, style) = { + import draw: * - if right == auto { - if left != none { - right = left; right.is-mirror = true - } else { - right = none - } + let offset = (0,0) + if axis.inset != none { + let (inset-low, inset-high) = axis.inset.map(v => util.resolve-number(ctx, v)) + offset = inset-low } - if top == auto { - if bottom != none { - top = bottom; top.is-mirror = true - } else { - top = none + let kind = _get-grid-type(axis) + if kind == 0 {return} + + if name == "angular" { + for (distance, label, is-major) in ticks { + let theta = distance * calc.pi * 2 + draw.line( + (0,0), + (radius * calc.cos(theta), radius * calc.sin(theta)), + stroke: if is-major and (kind == 1 or kind == 3) { + style.grid.stroke + } else if not is-major and kind >= 2 { + style.minor-grid.stroke + } + ) + } + } else { + for (distance, label, is-major) in ticks { + circle( + (0,0), + radius: distance * radius, + stroke: if is-major and (kind == 1 or kind == 3) { + style.grid.stroke + } else if not is-major and kind >= 2 { + style.minor-grid.stroke + } + ) } } +} + +#let scientific-polar(size: (1, 1), + angular: none, + distal: none, + draw-unset: true, + name: none, + ..style) = { + import draw: * group(name: name, ctx => { let (w, h) = size @@ -60,53 +81,46 @@ style = _prepare-style(ctx, style) // Compute ticks - let x-ticks = compute-ticks(bottom, style) - let y-ticks = compute-ticks(left, style) - let x2-ticks = compute-ticks(top, style) - let y2-ticks = compute-ticks(right, style) + let x-ticks = compute-ticks(angular, style) + let y-ticks = compute-ticks(distal, style) + let radius = calc.min(w,h) // Draw frame if style.fill != none { on-layer(style.background-layer, { - rect((0,0), (w,h), fill: style.fill, stroke: none) + circle( (0,0), radius: radius, fill: style.fill, stroke: none) + // rect((0,0), (w,h), fill: style.fill, stroke: none) }) } + let axes = ( + ("angular", x-ticks, angular), + ("distal", y-ticks, distal), + ) + // Draw grid + // To do: render radial and angular gridlines + // To do: divide radius by 2! group(name: "grid", ctx => { - let axes = ( - ("bottom", (0,0), (0,h), (+w,0), x-ticks, bottom), - ("top", (0,h), (0,0), (+w,0), x2-ticks, top), - ("left", (0,0), (w,0), (0,+h), y-ticks, left), - ("right", (w,0), (0,0), (0,+h), y2-ticks, right), - ) - for (name, start, end, direction, ticks, axis) in axes { - if axis == none { continue } + for (name, ticks, axis) in axes { + if axis == none { continue } let style = _get-axis-style(ctx, style, name) - let is-mirror = axis.at("is-mirror", default: false) - if not is-mirror { - on-layer(style.grid-layer, { - draw-grid-lines(ctx, axis, ticks, start, end, direction, style) - }) - } + on-layer(style.grid-layer, { + _draw-polar-grid-lines(ctx, name, axis, ticks, radius, style) + }) } + }) // Draw axes + // To do: Handle label placement + group(name: "axes", { let axes = ( - ("bottom", (0, 0), (w, 0), (0, -1), false, x-ticks, bottom,), - ("top", (0, h), (w, h), (0, +1), true, x2-ticks, top,), - ("left", (0, 0), (0, h), (-1, 0), true, y-ticks, left,), - ("right", (w, 0), (w, h), (+1, 0), false, y2-ticks, right,) - ) - let label-placement = ( - bottom: ("south", "north", 0deg), - top: ("north", "south", 0deg), - left: ("west", "south", 90deg), - right: ("east", "north", 90deg), + // ("angular", (0, 0), (w, 0), (0, -1), false, x-ticks, angular,), + ("distal", (0, 0), (0, h), (-1, 0), true, y-ticks, distal,), ) for (name, start, end, outsides, flip, ticks, axis) in axes { @@ -131,22 +145,7 @@ } }) - if axis != none and axis.label != none and not is-mirror { - let offset = vector.scale(outsides, style.label.offset) - let (group-anchor, content-anchor, angle) = label-placement.at(name) - - if style.label.anchor != auto { - content-anchor = style.label.anchor - } - if style.label.angle != auto { - angle = style.label.angle - } - - content((rel: offset, to: "axis." + group-anchor), - [#axis.label], - angle: angle, - anchor: content-anchor) - } + }) } }) diff --git a/src/plot.typ b/src/plot.typ index 51c73b9..72cd127 100644 --- a/src/plot.typ +++ b/src/plot.typ @@ -429,10 +429,8 @@ axes.scientific-polar( size: size, draw-unset: false, - bottom: axis-dict.at("x", default: none), - top: axis-dict.at("x2", default: auto), - left: axis-dict.at("y", default: none), - right: axis-dict.at("y2", default: auto), + angular: axis-dict.at("x", default: none), + distal: axis-dict.at("y", default: none), ) } diff --git a/tests/plot/polar/test.typ b/tests/plot/polar/test.typ index 2c7451c..1106e0c 100644 --- a/tests/plot/polar/test.typ +++ b/tests/plot/polar/test.typ @@ -6,19 +6,24 @@ #let line-data = ((-1,-1), (1,1),) #let data = (..(for x in range(-360, 360 + 1) { - ((x, calc.sin(x * 1deg)),) + ((x, calc.sin(x)),) })) /* Scientific Style */ #test-case({ plot.plot( - size: (5, 2), + size: (5,5), axis-style: "scientific-polar", - x-tick-step: 180, - y-tick-step: 1, - x-grid: "major", - y-grid: "major", + + x-tick-step: calc.pi / 8, + x-minor-tick-step: calc.pi / 16, + x-grid: "both", + + y-min: 0, y-max: 1, + y-tick-step: 0.5, + y-minor-tick-step: 0.125, + y-grid: "both", { - plot.add(data) + plot.add((t)=>0.5*(calc.sin(t)+1), domain: (0, 2*calc.pi), line: "raw") }) }) \ No newline at end of file From 3883f5e735949ae851e7f365a7bee24deb6d14d5 Mon Sep 17 00:00:00 2001 From: James R Swift Date: Tue, 30 Jul 2024 17:14:30 +0100 Subject: [PATCH 5/9] reimplement log --- src/axes/viewport.typ | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/src/axes/viewport.typ b/src/axes/viewport.typ index 034fa41..f98484b 100644 --- a/src/axes/viewport.typ +++ b/src/axes/viewport.typ @@ -9,35 +9,22 @@ // - vec (vector): Input vector to transform // -> vector #let transform-vec(size, x-axis, y-axis, z-axis, vec) = { - let (ox, oy, ..) = (0, 0, 0) - ox += x-axis.inset.at(0) - oy += y-axis.inset.at(0) + let (x,y,) = for (dim, axis) in (x-axis, y-axis).enumerate() { - let (sx, sy) = size - sx -= x-axis.inset.sum() - sy -= y-axis.inset.sum() + let s = size.at(dim) - axis.inset.sum() + let o = axis.inset.at(0) - let x-range = x-axis.max - x-axis.min - let y-range = y-axis.max - y-axis.min - let z-range = 0 //z-axis.max - z-axis.min + let transform-func(n) = if (axis.mode == "log") { + calc.log(calc.max(n, util.float-epsilon), base: axis.base) + } else {n} - let fx = sx / x-range - let fy = sy / y-range - let fz = 0 //sz / z-range + let range = transform-func(axis.max) - transform-func(axis.min) - let x-low = calc.min(x-axis.min, x-axis.max) - let x-high = calc.max(x-axis.min, x-axis.max) - let y-low = calc.min(y-axis.min, y-axis.max) - let y-high = calc.max(y-axis.min, y-axis.max) - //let z-low = calc.min(z-axis.min, z-axis.max) - //let z-hihg = calc.max(z-axis.min, z-axis.max) + let f = s / range + ((transform-func(vec.at(dim)) - transform-func(axis.min)) * f + o,) + } - let (x, y, ..) = vec - - return ( - (x - x-axis.min) * fx + ox, - (y - y-axis.min) * fy + oy, - 0) //(z - z-axis.min) * fz + oz) + return (x, y, 0) } // Draw inside viewport coordinates of two axes From 8e5dbde4b3448319fd014239285ab15e28f8bea8 Mon Sep 17 00:00:00 2001 From: James R Swift Date: Tue, 30 Jul 2024 17:21:29 +0100 Subject: [PATCH 6/9] merge un-failed --- src/axes.typ | 113 ++++++++++++++++++++++++++++++++++- src/axes/formats.typ | 139 +++++++++++++++++++++++++++++++++++++++++++ src/axes/ticks.typ | 78 +++++++++++++++++++++++- 3 files changed, 328 insertions(+), 2 deletions(-) create mode 100644 src/axes/formats.typ diff --git a/src/axes.typ b/src/axes.typ index cf3e78c..4c76bc3 100644 --- a/src/axes.typ +++ b/src/axes.typ @@ -1,4 +1,7 @@ #import "/src/cetz.typ": util, draw, vector, matrix, styles, process, drawable, path-util, process +#import "axes/style.typ": _prepare-style, _get-axis-style +#import "axes/preset.typ" +#import "axes/formats.typ" // Construct Axis Object // @@ -47,8 +50,116 @@ return axis } +// Get the default axis orientation +// depending on the axis name +#let get-default-axis-horizontal(name) = { + return lower(name).starts-with("x") +} + +// Setup axes dictionary +// +// - axis-dict (dictionary): Existing axis dictionary +// - options (dictionary): Named arguments +// - plot-size (tuple): Plot width, height tuple +#let setup-axes(ctx, axis-dict, options, plot-size) = { + import "/src/axes.typ" + + // Get axis option for name + let get-axis-option(axis-name, name, default) = { + let v = options.at(axis-name + "-" + name, default: default) + if v == auto { default } else { v } + } + + for (name, axis) in axis-dict { + if not "ticks" in axis { axis.ticks = () } + axis.label = get-axis-option(name, "label", $#name$) + + // Configure axis bounds + axis.min = get-axis-option(name, "min", axis.min) + axis.max = get-axis-option(name, "max", axis.max) + + assert(axis.min not in (none, auto) and + axis.max not in (none, auto), + message: "Axis min and max must be set.") + if axis.min == axis.max { + axis.min -= 1; axis.max += 1 + } + + axis.mode = get-axis-option(name, "mode", "lin") + axis.base = get-axis-option(name, "base", 10) + + // Configure axis orientation + axis.horizontal = get-axis-option(name, "horizontal", + get-default-axis-horizontal(name)) + + // Configure ticks + axis.ticks.list = get-axis-option(name, "ticks", ()) + axis.ticks.step = get-axis-option(name, "tick-step", axis.ticks.step) + axis.ticks.minor-step = get-axis-option(name, "minor-tick-step", axis.ticks.minor-step) + axis.ticks.decimals = get-axis-option(name, "decimals", 2) + axis.ticks.unit = get-axis-option(name, "unit", []) + axis.ticks.format = get-axis-option(name, "format", axis.ticks.format) + + // Axis break + axis.show-break = get-axis-option(name, "break", false) + axis.inset = get-axis-option(name, "inset", (0, 0)) + + // Configure grid + axis.ticks.grid = get-axis-option(name, "grid", false) + + axis-dict.at(name) = axis + } + + // Set axis options round two, after setting + // axis bounds + for (name, axis) in axis-dict { + let changed = false + + // Configure axis aspect ratio + let equal-to = get-axis-option(name, "equal", none) + if equal-to != none { + assert.eq(type(equal-to), str, + message: "Expected axis name.") + assert(equal-to != name, + message: "Axis can not be equal to itself.") + + let other = axis-dict.at(equal-to, default: none) + assert(other != none, + message: "Other axis must exist.") + assert(other.horizontal != axis.horizontal, + message: "Equal axes must have opposing orientation.") + + let (w, h) = plot-size + let ratio = if other.horizontal { + h / w + } else { + w / h + } + axis.min = other.min * ratio + axis.max = other.max * ratio + + changed = true + } + + if changed { + axis-dict.at(name) = axis + } + } + + for (name, axis) in axis-dict { + axis-dict.at(name) = prepare-axis(ctx, axis, name) + } + + return axis-dict +} + + + + #import "axes/viewport.typ": transform-vec, axis-viewport #import "axes/grid.typ": draw-grid-lines #import "axes/ticks.typ": place-ticks-on-line -#import "axes/preset.typ": scientific, scientific-polar, school-book \ No newline at end of file +#import "axes/presets/scientific.typ": scientific +#import "axes/presets/scientific-polar.typ": scientific-polar +#import "axes/presets/school-book.typ": school-book \ No newline at end of file diff --git a/src/axes/formats.typ b/src/axes/formats.typ new file mode 100644 index 0000000..0a3bf2d --- /dev/null +++ b/src/axes/formats.typ @@ -0,0 +1,139 @@ +// Compare two floats +#let _compare(a, b, eps: 1e-6) = { + return calc.abs(a - b) <= eps +} + +// Pre-computed table of fractions +#let _common-denoms = range(2, 11 + 1).map(d => { + (d, range(1, d).map(n => n/d)) +}) + +#let _find-fraction(v, denom: auto, eps: 1e-6) = { + let i = calc.floor(v) + let f = v - i + if _compare(f, 0, eps: eps) { + return $#v$ + } + + let denom = if denom != auto { + for n in range(1, denom) { + if _compare(f, n/denom, eps: eps) { + denom + } + } + } else { + (() => { + for ((denom, tab)) in _common-denoms { + for vv in tab { + if _compare(f, vv, eps: eps) { + return denom + } + } + } + })() + } + + if denom != none { + return if v < 0 { $-$ } else {} + $#calc.round(calc.abs(v) * denom)/#denom$ + } +} + +/// Fraction tick formatter +/// +/// ```example +/// plot.plot(size: (5,1), +/// x-format: axes.formats.fraction, +/// x-tick-step: 1/5, +/// y-tick-step: none, { +/// plot.add(calc.sin, domain: (-1, 1)) +/// }) +/// ``` +/// +/// - value (number): Value to format +/// - denom (auto, int): Denominator for result fractions. If set to `auto`, +/// a hardcoded fraction table is used for finding fractions with a +/// denominator <= 11. +/// - eps (number): Epsilon used for comparison +/// -> Content if a matching fraction could be found or none +#let fraction(value, denom: auto, eps: 1e-6) = { + return _find-fraction(value, denom: denom, eps: eps) +} + +/// Multiple of tick formatter +/// +/// ```example +/// plot.plot(size: (5,1), +/// x-format: axes.formats.multiple-of, +/// x-tick-step: calc.pi/4, +/// y-tick-step: none, { +/// plot.add(calc.sin, domain: (-calc.pi, 1.5 * calc.pi)) +/// }) +/// ``` +/// +/// - value (number): Value to format +/// - factor (number): Factor value is expected to be a multiple of. +/// - symbol (content): Suffix symbol. For `value` = 0, the symbol is not +/// appended. +/// - fraction (none, true, int): If not none, try finding matching fractions +/// using the same mechanism as `fraction`. If set to an integer, that integer +/// is used as denominator. If set to `none` or `false`, or if no fraction +/// could be found, a real number with `digits` digits is used. +/// - digits (int): Number of digits to use for rounding +/// - eps (number): Epsilon used for comparison +/// -> Content if a matching fraction could be found or none +#let multiple-of(value, factor: calc.pi, symbol: $pi$, fraction: true, digits: 2, eps: 1e-6) = { + if _compare(value, 0, eps: eps) { + return $0$ + } + + let a = value / factor + if _compare(a, 1, eps: eps) { + return symbol + } else if _compare(a, -1, eps: eps) { + return $-$ + symbol + } + + if fraction != none { + let frac = _find-fraction(a, denom: if fraction == true { auto } else { fraction }) + if frac != none { + return frac + symbol + } + } + + return $#calc.round(a, digits: digits)$ + symbol +} + +/// Scientific notation tick formatter +/// +/// ```example +/// plot.plot(size: (5,1), +/// x-format: axes.formats.sci, +/// x-tick-step: 1e3, +/// y-tick-step: none, { +/// plot.add(x => x, domain: (-2e3, 2e3)) +/// }) +/// ``` +/// +/// - value (number): Value to format +/// - digits (int): Number of digits for rouding the factor +/// -> Content +#let sci(value, digits: 2) = { + let exponent = if value != 0 { + calc.floor(calc.log(calc.abs(value), base: 10)) + } else { + 0 + } + + let ee = calc.pow(10, calc.abs(exponent + 1)) + if exponent > 0 { + value = value / ee * 10 + } else if exponent < 0 { + value = value * ee * 10 + } + + value = calc.round(value, digits: digits) + if exponent <= -1 or exponent >= 1 { + return $#value times 10^#exponent$ + } + return $#value$ +} \ No newline at end of file diff --git a/src/axes/ticks.typ b/src/axes/ticks.typ index b9928e3..75ef61c 100644 --- a/src/axes/ticks.typ +++ b/src/axes/ticks.typ @@ -138,6 +138,78 @@ return l } +// Compute list of linear ticks for axis +// +// - axis (axis): Axis +#let compute-logarithmic-ticks(axis, style, add-zero: true) = { + let ferr = util.float-epsilon + let (min, max) = ( + calc.log(calc.max(axis.min, ferr), base: axis.base), + calc.log(calc.max(axis.max, ferr), base: axis.base) + ) + let dt = max - min; if (dt == 0) { dt = 1 } + let ticks = axis.ticks + + let tick-limit = style.tick-limit + let minor-tick-limit = style.minor-tick-limit + let l = () + + if ticks != none { + let major-tick-values = () + if "step" in ticks and ticks.step != none { + assert(ticks.step >= 0, + message: "Axis tick step must be positive and non 0.") + if axis.min > axis.max { ticks.step *= -1 } + + let s = 1 / ticks.step + + let num-ticks = int(max * s + 1.5) - int(min * s) + assert(num-ticks <= tick-limit, + message: "Number of major ticks exceeds limit " + str(tick-limit)) + + let n = range( + int(min * s), + int(max * s + 1.5) + ) + + for t in n { + let v = (t / s - min) / dt + if t / s == 0 and not add-zero { continue } + + if v >= 0 - ferr and v <= 1 + ferr { + l.push((v, format-tick-value( calc.pow(axis.base, t / s), ticks), true)) + major-tick-values.push(v) + } + } + } + + if "minor-step" in ticks and ticks.minor-step != none { + assert(ticks.minor-step >= 0, + message: "Axis minor tick step must be positive") + if axis.min > axis.max { ticks.minor-step *= -1 } + + let s = 1 / ticks.step + let n = range(int(min * s)-1, int(max * s + 1.5)+1) + + for t in n { + for vv in range(1, int(axis.base / ticks.minor-step)) { + + let v = ( (calc.log(vv * ticks.minor-step, base: axis.base) + t)/ s - min) / dt + if v in major-tick-values {continue} + + if v != none and v >= 0 and v <= 1 + ferr { + l.push((v, none, false)) + } + + } + + } + } + } + + return l +} + // Get list of fixed axis ticks // // - axis (axis): Axis object @@ -199,7 +271,11 @@ } } - let ticks = compute-linear-ticks(axis, style, add-zero: add-zero) + let ticks = if axis.mode == "log" { + compute-logarithmic-ticks(axis, style, add-zero: add-zero) + } else { + compute-linear-ticks(axis, style, add-zero: add-zero) + } ticks += fixed-ticks(axis) return ticks } From 42a4959d1af8d7269c6feac04c6ccc84a97250e1 Mon Sep 17 00:00:00 2001 From: James R Swift Date: Tue, 30 Jul 2024 21:01:43 +0100 Subject: [PATCH 7/9] better examples --- src/axes/presets/scientific-polar.typ | 104 +++++++++++++++++++++----- src/axes/viewport.typ | 35 ++++++--- src/plot.typ | 5 ++ tests/plot/polar/test.typ | 24 +++++- 4 files changed, 137 insertions(+), 31 deletions(-) diff --git a/src/axes/presets/scientific-polar.typ b/src/axes/presets/scientific-polar.typ index e79c4d0..fa30033 100644 --- a/src/axes/presets/scientific-polar.typ +++ b/src/axes/presets/scientific-polar.typ @@ -39,8 +39,11 @@ for (distance, label, is-major) in ticks { let theta = distance * calc.pi * 2 draw.line( - (0,0), - (radius * calc.cos(theta), radius * calc.sin(theta)), + (radius / 2, radius / 2), + ( + radius * (calc.cos(theta) + 1) / 2, + radius * (calc.sin(theta) + 1) / 2 + ), stroke: if is-major and (kind == 1 or kind == 3) { style.grid.stroke } else if not is-major and kind >= 2 { @@ -51,11 +54,11 @@ } else { for (distance, label, is-major) in ticks { circle( - (0,0), - radius: distance * radius, + (radius / 2, radius / 2), + radius: distance * radius / 2, stroke: if is-major and (kind == 1 or kind == 3) { style.grid.stroke - } else if not is-major and kind >= 2 { + } else if not is-major and (kind >= 2) { style.minor-grid.stroke } ) @@ -63,6 +66,57 @@ } } +#let _draw-polar-axis-line(center, radius, axis, is-horizontal, style) = { + let enabled = if axis != none and axis.show-break { + axis.min > 0 or axis.max < 0 + } else { false } + + if enabled { + // let size = if is-horizontal { + // (style.break-point.width, 0) + // } else { + // (0, style.break-point.width, 0) + // } + + // let up = if is-horizontal { + // (0, style.break-point.length) + // } else { + // (style.break-point.length, 0) + // } + + // let add-break(is-end) = { + // let a = () + // let b = (rel: vector.scale(size, .3), update: false) + // let c = (rel: vector.add(vector.scale(size, .4), vector.scale(up, -1)), update: false) + // let d = (rel: vector.add(vector.scale(size, .6), vector.scale(up, +1)), update: false) + // let e = (rel: vector.scale(size, .7), update: false) + // let f = (rel: size) + + // let mark = if is-end { + // style.at("mark", default: none) + // } + // draw.line(a, b, c, d, e, f, stroke: style.stroke, mark: mark) + // } + + // draw.merge-path({ + // draw.move-to(start) + // if axis.min > 0 { + // add-break(false) + // draw.line((rel: size, to: start), end, mark: style.at("mark", default: none)) + // } else if axis.max < 0 { + // draw.line(start, (rel: vector.scale(size, -1), to: end)) + // add-break(true) + // } + // }, stroke: style.stroke) + } else { + draw.circle(center, radius: radius, stroke: style.stroke, mark: style.at("mark", default: none)) + } +} + +#let place-ticks-on-radius(ticks, center, radius, style) = { + +} + #let scientific-polar(size: (1, 1), angular: none, distal: none, @@ -119,8 +173,8 @@ group(name: "axes", { let axes = ( - // ("angular", (0, 0), (w, 0), (0, -1), false, x-ticks, angular,), - ("distal", (0, 0), (0, h), (-1, 0), true, y-ticks, distal,), + ("angular", (radius/2, radius/2), (radius/2, radius), (0, -1), false, x-ticks, angular), + ("distal", (radius/2, radius/2), (radius, radius/2), (1, 0), true, y-ticks, distal,), ) for (name, start, end, outsides, flip, ticks, axis) in axes { @@ -134,19 +188,33 @@ end = vector.add(end, padding) } - let (data-start, data-end) = _inset-axis-points(ctx, style, axis, start, end) - let path = _draw-axis-line(start, end, axis, is-horizontal, style) - on-layer(style.axis-layer, { - group(name: "axis", { - if draw-unset or axis != none { - path; - place-ticks-on-line(ticks, data-start, data-end, style, flip: flip, is-mirror: is-mirror) - } - }) + if (name == "angular"){ + let (data-start, data-end) = _inset-axis-points(ctx, style, axis, start, end) - - }) + let path = _draw-polar-axis-line(start, radius/2, axis, is-horizontal, style) + on-layer(style.axis-layer, { + group(name: "axis", { + if draw-unset or axis != none { + path; + place-ticks-on-radius(ticks, start, radius/2, style) + } + }) + }) + } else { + + let (data-start, data-end) = _inset-axis-points(ctx, style, axis, start, end) + + let path = _draw-axis-line(start, end, axis, is-horizontal, style) + on-layer(style.axis-layer, { + group(name: "axis", { + if draw-unset or axis != none { + path; + place-ticks-on-line(ticks, data-start, data-end, style, flip: flip, is-mirror: is-mirror) + } + }) + }) + } } }) }) diff --git a/src/axes/viewport.typ b/src/axes/viewport.typ index f98484b..cbce0ba 100644 --- a/src/axes/viewport.typ +++ b/src/axes/viewport.typ @@ -9,19 +9,36 @@ // - vec (vector): Input vector to transform // -> vector #let transform-vec(size, x-axis, y-axis, z-axis, vec) = { - let (x,y,) = for (dim, axis) in (x-axis, y-axis).enumerate() { - let s = size.at(dim) - axis.inset.sum() - let o = axis.inset.at(0) + let (x,y,) = if ( + x-axis.at("polar", default: false) or + y-axis.at("polar", default: false) + ) { - let transform-func(n) = if (axis.mode == "log") { - calc.log(calc.max(n, util.float-epsilon), base: axis.base) - } else {n} + let radius = calc.min(..size) + let x-norm = (vec.at(0) - x-axis.min) / (x-axis.max - x-axis.min) + let y-norm = (vec.at(1) - y-axis.min) / (y-axis.max - y-axis.min) + let theta = 2 * calc.pi * x-norm - calc.pi/2 + let dist = (radius/2) * y-norm + let x = dist * calc.cos(theta) + let y = dist * calc.sin(theta) - let range = transform-func(axis.max) - transform-func(axis.min) + (radius/2 + x, radius/2 + y) - let f = s / range - ((transform-func(vec.at(dim)) - transform-func(axis.min)) * f + o,) + } else { + for (dim, axis) in (x-axis, y-axis).enumerate() { + let s = size.at(dim) - axis.inset.sum() + let o = axis.inset.at(0) + + let transform-func(n) = if (axis.mode == "log") { + calc.log(calc.max(n, util.float-epsilon), base: axis.base) + } else {n} + + let range = transform-func(axis.max) - transform-func(axis.min) + let f = s / range + + ((transform-func(vec.at(dim)) - transform-func(axis.min)) * f + o,) + } } return (x, y, 0) diff --git a/src/plot.typ b/src/plot.typ index c8bcd47..4918bf2 100644 --- a/src/plot.typ +++ b/src/plot.typ @@ -310,6 +310,11 @@ // Set axis options axis-dict = axes.setup-axes(ctx, axis-dict, options.named(), size) + if (axis-style == "scientific-polar"){ + axis-dict.x.polar = true + axis-dict.y.polar = true + } + // Prepare styles for i in range(data.len()) { if "style" not in data.at(i) { continue } diff --git a/tests/plot/polar/test.typ b/tests/plot/polar/test.typ index 1106e0c..dfbc684 100644 --- a/tests/plot/polar/test.typ +++ b/tests/plot/polar/test.typ @@ -11,19 +11,35 @@ /* Scientific Style */ #test-case({ + + draw.set-style( + axes: (tick: (label: (position: "south"))), + legend: (stroke: none, achor: "west") + ) + plot.plot( - size: (5,5), + size: (16,9), axis-style: "scientific-polar", - x-tick-step: calc.pi / 8, + x-tick-step: calc.pi / 4, x-minor-tick-step: calc.pi / 16, x-grid: "both", + x-min: 0, x-max: 2 * calc.pi, - y-min: 0, y-max: 1, + y-min: -1, y-max: 1, y-tick-step: 0.5, y-minor-tick-step: 0.125, y-grid: "both", + + legend: "east", { - plot.add((t)=>0.5*(calc.sin(t)+1), domain: (0, 2*calc.pi), line: "raw") + plot.add( + calc.sin, + domain: (0, 2* calc.pi), + line: "raw", + samples: 100, + // fill: true, + label: $sin(x)$ + ) }) }) \ No newline at end of file From ee26caf31f9c043b89920ceffa50779a053a37ef Mon Sep 17 00:00:00 2001 From: James R Swift Date: Wed, 31 Jul 2024 10:55:55 +0100 Subject: [PATCH 8/9] Add ticks to angular axis --- src/axes/presets/scientific-polar.typ | 89 ++++++++++++++++++++++----- src/axes/viewport.typ | 2 +- tests/plot/polar/test.typ | 19 ++++-- 3 files changed, 87 insertions(+), 23 deletions(-) diff --git a/src/axes/presets/scientific-polar.typ b/src/axes/presets/scientific-polar.typ index fa30033..227ba25 100644 --- a/src/axes/presets/scientific-polar.typ +++ b/src/axes/presets/scientific-polar.typ @@ -7,10 +7,8 @@ // Default Scientific Style #let default-style-scientific-polar = util.merge-dictionary(default-style-scientific, ( - left: (tick: (label: (anchor: "east"))), - bottom: (tick: (label: (anchor: "north"))), - right: (tick: (label: (anchor: "west"))), - top: (tick: (label: (anchor: "south"))), + distal: (tick: (label: (anchor: "north-east", offset: 0.25))), + angular: (tick: (label: (anchor: "center", offset: 0.35))), stroke: (cap: "square"), padding: 0, )) @@ -39,10 +37,10 @@ for (distance, label, is-major) in ticks { let theta = distance * calc.pi * 2 draw.line( - (radius / 2, radius / 2), + (radius, radius), ( - radius * (calc.cos(theta) + 1) / 2, - radius * (calc.sin(theta) + 1) / 2 + radius * (calc.cos(theta) + 1), + radius * (calc.sin(theta) + 1) ), stroke: if is-major and (kind == 1 or kind == 3) { style.grid.stroke @@ -54,8 +52,8 @@ } else { for (distance, label, is-major) in ticks { circle( - (radius / 2, radius / 2), - radius: distance * radius / 2, + (radius, radius), + radius: distance * radius, stroke: if is-major and (kind == 1 or kind == 3) { style.grid.stroke } else if not is-major and (kind >= 2) { @@ -113,7 +111,62 @@ } } -#let place-ticks-on-radius(ticks, center, radius, style) = { +#let place-ticks-on-radius(ticks, center, radius, style, flip: true) = { + + // Early exit + let show-label = style.tick.label.show + if (show-label not in (auto, true)) {return} + + let def(v, d) = { + return if v == none or v == auto {d} else {v} + } + + for (distance, label, is-major) in ticks { + + // Early exit for overlapping tick + if (distance == 1){continue} + + let theta = (2 * distance) * calc.pi + let dist = radius + + let offset = style.tick.offset + let length = if is-major { style.tick.length } else { style.tick.minor-length } + if flip { + offset *= -1 + length *= -1 + } + + let a = dist + offset + let b = a + length + + draw.line( + (a * calc.sin(theta) + radius, a * calc.cos(theta) + radius), + (b * calc.sin(theta) + radius, b * calc.cos(theta) + radius), + stroke: style.tick.stroke + ) + + if (label != none){ + let offset = style.tick.label.offset + if flip { + offset *= -1 + length *= -1 + } + // let c = vector.sub(if length <= 0 { b } else { a }, + // vector.scale(norm, offset)) + + let c = a - offset + + let angle = def(style.tick.label.angle, 0deg) + let anchor = def(style.tick.label.anchor, "center") + + draw.content( + (c * calc.sin(theta) + radius, c * calc.cos(theta) + radius), + [#label], + angle: angle, + anchor: anchor + ) + } + } } @@ -131,18 +184,20 @@ let style = style.named() style = styles.resolve(ctx.style, merge: style, root: "axes", - base: default-style-scientific) + base: default-style-scientific-polar) style = _prepare-style(ctx, style) // Compute ticks let x-ticks = compute-ticks(angular, style) let y-ticks = compute-ticks(distal, style) - let radius = calc.min(w,h) + let radius = calc.min(w,h) / 2 + + style.fill = luma(95%) // Draw frame if style.fill != none { on-layer(style.background-layer, { - circle( (0,0), radius: radius, fill: style.fill, stroke: none) + circle( (radius,radius), radius: radius, fill: style.fill, stroke: none) // rect((0,0), (w,h), fill: style.fill, stroke: none) }) } @@ -173,8 +228,8 @@ group(name: "axes", { let axes = ( - ("angular", (radius/2, radius/2), (radius/2, radius), (0, -1), false, x-ticks, angular), - ("distal", (radius/2, radius/2), (radius, radius/2), (1, 0), true, y-ticks, distal,), + ("angular", (radius, radius), (radius, radius*2), (0, -1), false, x-ticks, angular), + ("distal", (radius, radius), (radius, radius*2), (-1,0), true, y-ticks, distal,), ) for (name, start, end, outsides, flip, ticks, axis) in axes { @@ -192,12 +247,12 @@ if (name == "angular"){ let (data-start, data-end) = _inset-axis-points(ctx, style, axis, start, end) - let path = _draw-polar-axis-line(start, radius/2, axis, is-horizontal, style) + let path = _draw-polar-axis-line(start, radius, axis, is-horizontal, style) on-layer(style.axis-layer, { group(name: "axis", { if draw-unset or axis != none { path; - place-ticks-on-radius(ticks, start, radius/2, style) + place-ticks-on-radius(ticks, start, radius, style) } }) }) diff --git a/src/axes/viewport.typ b/src/axes/viewport.typ index cbce0ba..8b2def8 100644 --- a/src/axes/viewport.typ +++ b/src/axes/viewport.typ @@ -18,7 +18,7 @@ let radius = calc.min(..size) let x-norm = (vec.at(0) - x-axis.min) / (x-axis.max - x-axis.min) let y-norm = (vec.at(1) - y-axis.min) / (y-axis.max - y-axis.min) - let theta = 2 * calc.pi * x-norm - calc.pi/2 + let theta = 2 * calc.pi * x-norm let dist = (radius/2) * y-norm let x = dist * calc.cos(theta) let y = dist * calc.sin(theta) diff --git a/tests/plot/polar/test.typ b/tests/plot/polar/test.typ index dfbc684..f216f47 100644 --- a/tests/plot/polar/test.typ +++ b/tests/plot/polar/test.typ @@ -12,10 +12,10 @@ /* Scientific Style */ #test-case({ - draw.set-style( - axes: (tick: (label: (position: "south"))), - legend: (stroke: none, achor: "west") - ) + // draw.set-style( + // axes: (tick: (label: (position: "south"))), + // legend: (stroke: none, achor: "west") + // ) plot.plot( size: (16,9), @@ -25,6 +25,7 @@ x-minor-tick-step: calc.pi / 16, x-grid: "both", x-min: 0, x-max: 2 * calc.pi, + x-format: plot.formats.multiple-of, y-min: -1, y-max: 1, y-tick-step: 0.5, @@ -38,8 +39,16 @@ domain: (0, 2* calc.pi), line: "raw", samples: 100, - // fill: true, label: $sin(x)$ ) + + plot.add( + (t)=>calc.pow(calc.sin(t),2), + domain: (0, 2* calc.pi), + line: "raw", + samples: 100, + label: $sin^2 (x)$ + ) + }) }) \ No newline at end of file From ce415e869d8f5feeb36f3a122f17c132eb438dcc Mon Sep 17 00:00:00 2001 From: James R Swift Date: Wed, 31 Jul 2024 11:14:07 +0100 Subject: [PATCH 9/9] add test ref --- tests/plot/polar/ref/1.png | Bin 0 -> 81317 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/plot/polar/ref/1.png diff --git a/tests/plot/polar/ref/1.png b/tests/plot/polar/ref/1.png new file mode 100644 index 0000000000000000000000000000000000000000..aae402d9b2c1817a78b71954f3e2dd49c3256165 GIT binary patch literal 81317 zcmcG#Wl&w)5hv4q+?oROF?he5T?(XjH8vNkyPSD^!+;?;Do0|FZ{!=v{ zPVG}&wbtri@Ah6hOhHZ@9tH;n006*CN{A={0AO1mKLsez4+=$0oIU`A_nV}Mpo-hd z=>ieR2eFUwDLDk^J6+$rG61k8M*2YtL;wJ=oB#mEBLKip5b}Q^z;XS(q$daL?QZj3O5~QBAt~iysv!j;BUc4w^P2fJWk|Bbk3-`2Q*0|4~%`FX{gOC_CT9 z+U#pe$+#P#)ssp%t{B;VO^i5FNrpMrjnN!qAem&G;Bd}oCrNg#-xX2$TrcXq)$;KX z-rO!0BnZ14S~QMOMBpWIckB?}U?-yy0s^2Wh3qk? z!vnvg2-Q(2Clv^Lz@u&Vlo~J)PW9UYBN@E5nMu3(eC$xCuEDuLGku|Ng5?8s-Lvj# zFAkFn7ptZClQ2H{x^`EpZFE1s+)Z3us3u?WToiWBSA**2T#x(Ps9f^P$jMbzSDVGO zt)Kt=`Sbj|Y7C$S>L0BrI8D#MK(e#9M>&b0oR&o4N*xmCa4x zCr{wx3<8ZHZSCegA zvuXtKtX8jk59Oz))L(n9ukUCCr2PaA6+V0q5CxozL*>I&CIS2>b3Yty4jGh_UCDEN$!a|>;TjF{C2R(MpF3-_zfW+8_c6;!RHX9X5#t54U^pEF2ENM_$Pw6u1( z5&xWRBPO-&=Jla9wk-ue>(*OVlU^mnCD*$)ET^`K6bc9R2B?fzymq*;+@I
    h)E}H8A4U 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⪙RkrNXkieHVh!r(A8Ha512=O{B=I5v;Z zW(-bq`AwMF?6Ugf`RTsUXT?F!#Ct(-$yl~_z#^L3XYQ>qd37piTBwguoaUmHb`&l$ z%JcT#BH$}vDsn=WHIEJvOvb^F4i~&C)HS;)a2@m52;nKt&aRtHzlnJ^JQN@TQ&SO8 z$O)31>zxGJOgCe%C)XRF)0{~bI;RvpuW0|hU4D?JvpsmNJ(GJNqx;x%yP;wuO3HH_ zso4;+B4NwPZ&LS-b~j56(}no;UWC=9T)%S!QLKkc9Vb|T#15Fw5kA(;wKdM6LZoa@xc({Q;(;yr=_A#Yl~#jZiRx~J8-8F?oCklCJ)c-kCi z?kC~rg+moP4c2GB2X6-9iYreZ_D&T*ILiZZ4%TZt7#?(s00jo%PHyc{i(!XMD!fNE zaW86ML7zi!7*mAY-YIL5ekagMD6SZC&=xA;~{vUQ=$5HduIG0J~E=QmJbp~fxpvM`$@hY0(X7)<9R+uD;q2^xBE#=XHCB~Ug@a>9GEMK!x2K?6qIco0m z7eRy*f)KVOx(BEFK6}cy%p%5$%Mcf#+E}tg#^=2Uis2Es7slgG<8~KoMcbBi2=FE( z31~@WaG79`nAKTEev~|iOXi(1H+r6dktg@@BknKe)YS{6sNF;1f`o?X&FF%^G)Bbh`NB_iQE!qFGirrh$X#ezq83AsmZj` z>xz)7P$RXT#t$}Lw)$MRJKFvsMN|yw*q#5~Ku($|pW>TEZ(Dto5LowPMJhUmO&&<3 zK1}6!biAYt;0V)#oV%?x^w~El?@BgeB9-;=iM2 z{BgC=M^Kgw??SD-HB%DDJ*RdXXz#??b3KN(3u@GNNM-QD)19II1S3YdA+5Q;&nL5G zyo9y^J|3yf)?_>H=K&&$tKkW}N4GQotaR1hAmU0)tKib1GMEvttxVBB_F;0LQcAr^ z5zF3Ld$E1H6b?l(#`iPuh0g^IdSazkrv)*5A0;>OIcb zSo!t7by74$tT=Lsr>lW2Bs4_#?qruOp76_7xIEpR8!3fglaT z;Xi0-Keem7pWtmLBs5TaZ!-6~QwpvwO298#-F4>(D`GdrlM5WC_C4J#%QVH9B~Cg_ z&_i{|hZDUA(xu=6UDPZxC`Fj2_kgy|1T9Ym!@gj1Y#YZ&F@Kenx^GkvrKp7GZ$7MT zd>J{_8*4Xdo*Y6nI2UuV`C+eQ)r;+%$ofg)W(lUcb%x#_W~^NQO{TV_3-FHHFPYo0 z(O$3N1+hm@L+;PS^T4V?l~;ihq4%GgZ8L-h@w+E9y3X4-i`lKJ8{*@Q+lSX7YKwN# zW=!Af)sR21_9eXk)8@IWtVPR3lMWY2_TFV3UH9E|At923^I_rgNplBb!j@tpU7HOd zylkC#9|?m1Vx&S~0pLZ#^K!CyssBZt@T3Y^{y6lUH9?wLCfqX*qE|@EJ8Mbb@H$Xd zknCHt9@o1@g?M*=X{+scX+MK9EWQ)=mAwmQ-6qaZt?+6T%ON9ceDXF&CwR{SuWqeX z|Fsqk+*%v&a+BS)YAMK=-Px%305=?&GF5WB<$jltWFG`2hl<&E)@zWE2V?QtErWAa zR5HPEv62tb?+1I&1dJx|ROPxl;HsE+#5}0+|LD(lDxm4Sxh3Fh<5$3uxt@ErdF?=M z+&bIqVgaH_;?bpS(Vb7>=;eUO*|`=O1q?@TeD&eX8s?)X4&ab4LE31=pT2w>wo9-# z{@KMa9g7rQKPu^Cdi+ndsOl9%OqG8?2J*o8IO_MABBHJ5{-KDC;XJSSX_q2ZjPM35 zTCHOwGe&mJd^a|5ztIJ`uR(x>F8~L-%8IN&6!{|X3r6oAAgVE3n^;1-E*)>~wtln7 zy*UC$3|c?Vbs%U0mVl`O?g-T-KD+IB;s8MMF)Z2W^@Kah5CZM>Byj$0?opSQSDrXF z&bOaog!w{QnkzNG_uBdGUeG%X_10iK7glb(;@prK5FLN@LnCBz&#c3GxBjQ8k&{I% zyvb*_a@PDJH#4J1`*^dReBwC(GKUeXN)@U9K_6{ z&a^F}!ov#dKQ)8j7mO(?Qxq%|5dxOT*|NL|Y6SyuD!R{b5;Z~%QlsX*hBVz5h6RPd zlGjS_lYW*nq%{WheL#Oe5jn06Uj??=%7@rutn(G|y}ns{5AGkO6%??{Lrn#U?aKx4 zPnfQUhgNM>n|U>xX!keuyJ@~31Sm>8E6+?!(7xMA=F-IHrJ3;I11&7V!yTS*N6Cz| z*)*Bx`*0gci(FBSzt{EAcD1ul)$4GfUES}r_DY9bV#bF!p=TjJmM}kIkRT-_N+7_S zBy^&n1aKx?pY$;8X|_RwwNawbu<^I`tW; z!^vV+Nup8O&@QWb>LN^iR4n@FBo2yXg~eBpQAfpxu=_XBsWLS5;VWT;)N7-6T2+dQ zBf($_|L23b8>%bj_-rAI-ZyX@DD*`IP0Z}#=n4EOVkz{SSS zcke?F(rXUK3Q?XSa{zj~DJ>$=T z+HI4d2}@;i&R(QQ^6?Q_cLAFEVQ&LzJdb`!21@h`zFIBykqHp_B6RoT8u z+i*OOeq!aHQkT4(hYJrE|CWOXk`x9Uu^p91t_c$sDUk2g@H(nBt0&A-&oq(qTgRD( znbOiO16lfBlAiz4wQTKiJUWsW@0PcpB53XC^eU~>*$W1Vw1*?e@_vmCj7$GjSxsHy zJ1t5$Iy!?-BPCA;vlolu3+G@Pe};pB$8(Huc1iuuVu`dTPou)>$>1Qe?-J5oLiRy` zd~vKmQ7~fOc@SMmoR>jraX8!ar;bSE*9@e#_eF#)z6GkrBE>B!r>DDwvZ5%aPXbyM zrUFVaR85R)s4k~%wK$dsx_S10TfVDezS8<};$Pld;P?_tKWbsNRd+fA<%HT_nR_V8 zL$h*s9GO|~%tDM+cHZaKQsWgZs4ZeP;#W53sH<@61w=s@wa0ewY07}W=Hin-DESwQ z8}Pv?q0Ou<3s2^SuA4!#>-tKQ)8|$g-JZ}^lq=uBi%)yuzPG8z;z_qzKl|U#cL2mP za~D1G#7V~&fu80)V5xVSk>wy^9;?(G$-t)0i zLKhiA1?*asW7b;T;mTC1egylA$NvRemVtH^{g^+r+LrePVi1s!Y;OKk?3u_ESEHG~ zE-T6p4(kMGU`n;h?32{tPrMr0%#hRv`sAPd%t{v)%MLbR!MjotkHTX&ks2XWAf=gn zhipA~W+hh$2GT%|V|oK=6~gZp@n28CnVq zn}2%Ie1VYje?`6oH`|}_)4KvO?>{ojY9HmFykV~}YAEf(jxvs--$qW|25p4=Myq|z zA~O3$8EuKVXEwzu_Yj=G;@YQBj1wA60_1%49R3Z5yqqK1-2Ii6)Q9t7?SgHl>6sS( zpY{a7ulObUVhOzgZR!XXRauk^Ev831BxaOrV>1fz%kFRF!>8lPXcGp(vgB{O`MBol zQ>ZL!#9RL$Mh2!m<;A49Ym~e3+od+&72lW?*ld7qM~>c(U)w%|$Rc#q8~;~jdbC%j z_1PS+!Jl0n0dtK!^>T43BW3|Qn4bgS@2Hj{{93tEF?=C#c>Uw-`S#>lAUQBh!A*%x zs^y{2B$cWhXi7N~V=5Bzs9*G1nk%7Pe?R_=(A|X@bm=e3B7$ z(lJ`@N5|gKR zDlBVs9f_#5_i%dZ?ou1cto%>jN|Y~ScqY=srAirQb2|O`aB!H`cW_=q*+@Jmy#ISl zQKx$K3MKHlbdb^ck44fwYUXotC(M5aR`uI&pB8U@>317&ojHD%&S&QBHbY!Wo5~>z z@+*^D%sB733|-QuqQ-a@5Ct+J>6nJrw}=Ql!#Z0VXUiO+?_3ORyetlLNYbMz+}zjL zo2^rls^{PxG=f<) zSG)aunuj3sW%vEuOTs6K6v`LgGQ;om1A~$U<-j85>VWu{7 z&TP!L70xn$+Bb-He|uLgeD03L{Ukte`DNwp%5!FtI!@%`z!w<dkxqulTNmA-mnE+rqf|-*@RVSK{I0<@WYejgRy!js-2A8|nr(hoMk7b{;|qLICl1gK zgJ)L_3s895DomUf;^^(2E;wc3&}kmeTt!r*_me8u(ZL~>+x1+8KNoiJ$Z(5*yg=?cKr8Q0My}@%X>Y3eP{t8bA|)ET6qfU-CLiD@6TzPdva7Wz!k6 zFd_~A(`4}#xoVd@0dgwVSBW`m-<1A9hC*Z@22o*?#{4S}AIt)wYwcR64de=Ma=IXA zU?-RT*DW!AsMEHOOxP{(7ZH)OJEC*|n|G7mzx;>!t96ukb>8*k#f}*0HOSG_=_VA~ zzqWNh*FpCD0{02HS2(${MYnGC#pe3_QaIQQyPE@wj_5w{E$?*wq-&{ zK_w&~%+LBot(09av;1Yh@WXcoA-5?yowkqjpE~V86cZ=E=(|su=KU+waiML?sE-%t ztVKhPv_)PfFYl`rJT~}mJeGP0ew$`|#qZ#^AOC+$eT83?Tevktm!x!ecMlx`(jC$u zjpWcNUDBNcNS8=AN`rJGT{CopAo0yP=idAK{(_nJ*?T{^*0Tl~5vvgO=|p^PeLrHG zygXN8{m#YBJ*U2c17-XWOZHDZ8q^H{lC+#~NaqyrD zl`dfSzOE1X`e}U|)F{NawWsdK^&px91~a21D36@q!w}L!Z?agebf_~~k)TLW-Ijwg z^nSS_(~Vs|s(h)j7Gv78j3sOCF;N4wYo|QrY(m#8%n?SUo$W%ufgNK;MFZj^a ztoQM6yKzhCl8M$=?71hY&*v+3)T-aZhZQ)&uNz3YXl`)%U=O4yA4V5>;8#UR}ANf{HF;OtbjF5cv8W2lXzrt5N$=C5= z;P?YO>i_;IFIZh`4ZR>GA>Ow+Y5vR8i4fEN;IFtjVILAz;!DCWcyx7$g9hG?lrbZoy$G=Gv>{U@576hdER5N^?B)HoS< z&PKd=^LDYqzh5<}z=8hD)v|2Nh?bKaLw;>(gfY9kQ09!evL^>qB@f)hOo@Ww-S3u9 z{#)`Qu3I6T$NCPlYGGmDoet%gtKfYFq*KHezVg&o8Hd}~;-8oyrb8T7aPbYV-??v5jw!4v^5P-Yl0Oe~fn&qr!DDZwDHx>PHTm1*53Mq&Ywod=-96D>02vx%IP&3+ZJP3AkUd10 zk-RxGlbD~1ZQW?WU%W#hyKG{tbSgUHJ;H=A2s&-TXAP@Dp9+40w99c7zAW2!$vmu8 z3cDN?{m>?gT>TYmZDP-g|ANWMuRV+Tyi;AT_)&n+P(Bc=%&`p$&qq9p?_(^xcfic( zYXDIgJqN#VLTo=@Rw~8~;_0GKS38BOmJ-LobluK_8KUWV%Vr<+6PAAMi#H4$cO?Qq zhfC5!boGT83>}umQzhSo>sgpFvi06Zk&yJW5nUI+UBJk#(hgQ&8E<$MNVPQ&Dk22( zE|@p|>=flI;l5!qG`2l%dE-e)-k@sW^ml9h8ndILW4tlRPq_TyK?>tWD@ZP6my^#L(>4yffHq-lr zKT!{gZy=jc7V}5G$X9sLSVkWV*tUf|yw2sP1Jg5s5uzL4_{lAO?S4dX^wFAoM z5|K>=m$vsxXp122jYV~Few%uZ5PYH0ry#|VXy>|Eff-12#TyBoi?7n3jeCOTBVXM6@XhW z+mo3miyZft-#DD7G@~^MdHL_?3JrYq7a^^@Q8^vlI2Z!wpRGP-p+}gO!p61TV~emY z`V1B<$>`(<+JXnQGD2qrL0g&U(rBFpqSc$>LHU>4xI4GskO?1(*mbChQc=Y%mXB;W z(9mzxel-4FfJmG&e@b=Plrkn7q6x`bbO$usFHD%(z?l&J!Y1`Vp$i>hM=%kLm^5Mt z{Ii=w(xh~=h!?|?hERbU&qwNZUX^H;bEq?fT3T)u)v z`{m6W7|T5qGvEKI7&-WHJ01F{N?qdyr~4QGjtFIg{w!1r;PPj zg`0Yapx=!2mCCIBR_MJtYTdAb49)vZ2S}xn>7A@BiixaDF=HAtNHb$Hoi(ou8`=Cj z$V;~UbBSuVw<%+_%Ooi@?+Tl&@iSM_6Ojx%9G}vAx7EMF5ZNND34>(9|I#(X8`Z(w z0`(Wu=1NAr52washbnTb4aQ*YYV z?GPnnCEbnc*i@)g5*>V{4~nrq0VMoY%@QP^XolV>);6LJqMWu4u#~5!EP}X8Gv$x( z7!zP2)_>g>8{A9fg=UDHkFl-e<{8jP?=>;`;mQRa+D?s8mCVf_%}wR;;Hu%{`7D-k zN@4^lKC{p&x5^a#PfB4Z1al7s_myLXjd9C@$y0=Ol%uboZ`CBIlj6vCZ+al1fk(Z!vsZf?8annIwL{4`V{d8slI_8-63b#EZv>1CTOdHToFCQZL@5 zj}OZXdQPwjJgr{5lURIij~yxUkK43QlzjPPqU&t-@N2l0089i++{Z#V#dkuD>ocw8 zf2RdMJa5nUZbeE(G1f&SHZ8lza+zYM*WMhjF#~UsWv^oL#CVid@UD14)HUSqONdd= z^h>XWS2}#s1HM^R8+`(Fi!*KW#SQ4NjT0imHCveatD>|og{$!BOqf;DFzUC4Q)teH zadS=1;K`@6*)!cn$!B-U?F9<1E&GX&$p4u-)IL5lTzowv+OUWdfBGrfDn!D>XKC{T z^Ki$QYC!LwR2}Bg#w-CHQ(9@Q<-g1%1yF(YPw_46uirCUnIk!w)CvsmtrN#I4krjm z{8C0$FMwP&IVfzzrbNXI9wdG}xb71Omcjx@<^y&PqPmCkL$Nu{3PRs#uM2GMkf0Oo zvC}K4U%ssSmKZ0Jeic3!!b+a26_G3cV6|^K&=@~!BByputO8rPViqYh5ZNHb8QTEG zIDJ^4sz2unWh_!qM_oA6`Q-NNsI6cy^@7;Y7&i0o-f*#hIFDEe<0AamAnFvVfTFr_si;+^ZMsAe}JFr?XSwm$01XCIR%#L5fbUCYxMBrYp{3xGX=&nIJ- zFRUh4DT&9$cdf7KCo}LJ%q2MdPhw^5+iv+COhA<;Y!yq zukv*klcI6^9uahIl{_cjz}%oCm z)(>j4ua}fjSP;%W5`OLK3vuT9S={^d_=rB4W&A_wr~IZNwT1^O+>Ymru7uGS^I|HJ z>`glY*J<@HTBXX`c2-=jM0*k}K0xn*NDrFkhRF zgmeQNHY)nQh(5qIjoG$e&p-wFQ9FE7_y7J4vs%L5akJJ=L&JtrynkuLa%tlo zyhFqBKz635Q+-qLW>WqmUbL^)ll@`4cnQjckXFVlzhFq6Zq=DkhlM)C&yw2@0!%#n z7E&Uvz0ms0}u7S#|!EnGRe!p2wA0$9ATwXPhW3SgMYd?Tjb8{2^ zMpcv3w9Dtu#rDfFmq|xcSC{d>MdmSrpTyKFe3!je75l=b@ALduCedIJ1*5CowZ8lJ zsl9^WTY_xp4P;;AC;jYhJfxw&ov ztK!`*51HP8g$$p2AFo?^-92~aM;_V_j;D?VK!JeAolKa*^7(pa=DN^fU1@1)7z*~k z7nenfO4#3X2_ZNxRX*+UdI;R}(u{BWb9(=R46^hl;^k$mzILh*o4la4-NjdYBnDf8 zcG%Sz_cD801iWH9oMx|NwRcKlVpTd-gU=dcw-bd$(cxHjveuqT3U&90E4nV@*^muO z{+cKT{9cFU&&-PD-MfH(tAHLTyVuG~Qr>ckl6Yz+G(;=n=koD3*x%tKr?QUAsy&uB zRJ!a}njmM6v0yv&+4RRgK7k_@M+aUlY1 z^}~LBrj%Al&&)oItd!Tj4lA+hyYE)}PKOhzHeth&<&pP3MmZYr8D1C^?NdGzd7FV44OOu^BfTN%PKV+ zQ(HG4Ha9Ir&RCfF4Fp#rDyH+A3PM90!Q?NbS(CSsld2Vj3pNQzDM3iaR459Je``KC zh8O=%iPvths|&caXz7c8Z*U?TaBa~!!j#wbbCzr_6v`t2G(F^l6)a^eC3RAc=VnF zZR&xI&8+dCtdWmFbK2^i&Ao;P>#uiz&%W!Knf*2K?{=*9KD9l6Hk$HVyml&tA>ywg zQ*pKWX1ziAVLw;zVHcNCNTnMuB z@-S$dB>n!`n|MJbMaFMZ-t9iRVCXp1=q#&UP&O@!uTXLtXgSAN;(=+PElX$_52LCF zs_o3deakJB&kmVH81$8kWTE1z(SP!q6H=PBIBHF3UqIL4uxlg3iJvQQahvl)0EbYC`MAR0AVOJmai@9YEE-4t zQBS4-4v-aNh14O?exn_(S=@b!Dzb23#bzYLcn@Xm3zSTrYhk-tEWP%bSmKuee{=0t zGW@~)*OL!)(06kj>D}S5cr$%L*t?F)TBqbK&ot+GCt=bgAq>u4$e>GZtd!20Yxic{ z+0CnhmLn}Sk*~YQw>V|I0~}-SO5qCvaQMF~^}V)zJPJ80gCsg$aVlwz&9PBT-)@z? z?7@`#a?INHCwF zMwsxb%JedFkKIghPvGCHM|)Io7{)$;r~cuni}Cgf+|Kj3P*@22ML#)!_B?U1o0Z*C z{k7@T6)=@BsZ}p(S{joYh#c|JBvK*No7SgJ!TXPwEouA%3>6)(=`=Is^qG9e#9M7g zxK0?7_F%wYapn7=p%z|kj~^nZh9$t)j7o2oM7}*qz%V?pwd1?@dY)K5pgI*+**P6t z+4Zuw9ZHLbR6zgpHXSDHa5x9kJSG=%HtzH~#p&`!pVkcLFlxapC=mC*3q`^hN~BU} z4C8j+8~KcY;&r?@fxHLPUd$sEbTW8-c_w)+QAnIDlnlPw8^vc-X$$=8rD0nr5ja^O z{+rRuWv$I^D4rysp|mvOUKA)yTFOd^A-&C_UFNj>jRxQcf0I^)1cd>du93dOB-Spc z#k|jqDd0@7;kmjplnm`Szg^U_^H13a?U2d8lk%RfL7Qgb>N+UB6f%wS{<1W&8h$l& z{F-WvuzTn;t-8vK9odx9?8zfUR|nnyXZHC;Q$-h3GFiQ$?|X!{PV_H-{<)_D6E%`K zA`el;620JDIm*Oa?T!3#E=%HZJ{$kFtNeE?u~`MfUp8-=>BLuig#pv$i$L%&er~xk z(v2=hZ(!T``;r$s@y#jNe(ZYFZs6aUjV`-kp9Ix>V7hAY+DXRs)(-^p9GVZU&J+wq zpRSlxbLOk{xio&WZO15(I4=uwjBmR)njg-VVx$(sz=At$Ww518My<|o(CZBwBeCfJ zebUWv`TD%)lA`Uf=y$f>*{EJB5mMJ{Q-uKS8bWsx(8fdy=!YiNQzFGXFSSO#bWWXg zxTwxqMFT(4$rwHZx2(HNQ;k%Rf3$5PvWMvR-}g~cJkgS9g;l}&HGJ|#+_iHg`1(Bs z&7zRBB99)=tBF4nJ1tx?d!SLW?^b-hxNw@hrnAo!n!Tt{cK-H5-_I{>zdI`n>ESxe zyYzD$N*+TbY|imHz5g>sgVk{H>|J8TXcRWp8P!s~*?EZb;l*}8CN8V%)JnStlZbx3 z>1`gFkaJS%QgqmXs;a8jum0g(ByUHvnnjMe`083=sy(;D+|$&o$Zgin8lgPxt?Tym)qC!&oh2Kg)k-QljVk87(z9HN^ez>Nl%Ptl4wv(fD@9tD=! zGj4PJ;X&-mzw^*?F}9;yBmgmDQp<~u--f*{zx%WNe@&2%0LqxUJKyYu1whRbc>y7z z<1bPVcUg64(@0-@5CT}BljCwO19hYWWs5pt0B{(Rj4&PmYHdb^ig3FWj+>U)RIu-P z?KuMYN?>lchMvcTtamsGoySbb<;+;B0(1l-RU%jBPfjcjq9rCIlNj_VM6SyW`$zcF zCL+{P7AxO_&pVOEswDB{=b?EFjLu2-xs-0k<^ghGp~{McSLUD^uv@(#SYMQ<{@@Kp zL@LnxEdD*(_ZvMah&3r35I`sv)i&rWHI-Yh!AHcSh5U*Gb0sPv1#se}u;do%93x7i z>$eUtX^o%HkBHbv1FT)~?@)5eB@!u`YZ~eAQ^VcFh9_Q%rvd4X0HcRuDK3a2ak?FEZJsl#*lh`5(oob&Vgi37UkXbJU0}lMzp{8jZbJYF zrrmy}a6nra^OOWjkYhkK?jBt{us7%#56mQ`UMUmu@_6gm)doirRN!NCLRF6qJ|=Yw z#Q*ASS>!n2NqvV}axgzg%kuK_?3^R3kQ*}|jY}tNoh%_TxMbp{=E7oQq?R0BgN9DEZ7(ZTF(19=xi`p3 zI8N8>hg4+ps8?{oZMhG4o~v)Ad#c?G>gOplJ8LNgN4;DLoB_dflGcuE_~2u-V9bPF z`aGV#Mg2NsO*jfonkbA%voG$Qp3rH)5|82Bo#3!5EY(Jv#s`<{&`6=M_%J*DZkbTy z#f41(@TZbH0I(H?OP>ZjrIrMCI?&M7Oklb)aKK$IIPd4Qwpcv=9$Ee^Pjuq?ITG#g z&MrfC^`PX3FGb%J^opO7=@c-hAJC~(6($>W$9cjB8uXF+W;FB^)BsHG>)_4%bo8HG z>}K%kIUWrCz$M5hu5he3=A9z z*8#>QavIQ;pApyr&V|Rn+{{H7Z%PKWF~c=!n6`wvO1ac5e7**LoBShM-x-vzT}H<= zOp^c4tDzIMwo8Cle4CjvQ-WYfD-`Cx*ZXp(({a&R?HSlosSZazJr%UE2jWDveda&w zAfhnYty8F=KA`wXB~)Se>C=Ab4lcRtz-OVWDVZ}-cxV{%n=5WQ4FRf8*u*lzfsY`K zTtL9(4pR0SW3^tbW%ReG27Am_vA+W&+X53B{>{tHjS=k897;TQUuBMjg_&B=&df&2 z?a2i?c>K(f!)##Q^PhNX9X46pV?W1eGc{mq+5uuooD`G=?CKCSwqv`a`4hWJBZ7ns zart*r$fPNzNx~!fV)-7^?XNeV`ZpfB=yBxS0uuf1c6zrAE5eAQsl$_!z)`Te^CcvK zarp(!A4<4qLBoq(5HAD7z#|NQjarg;ltM1cn6VeI?};LAfwV~u|VEFfLkX6eA(y{7Rn-q*9oUj=QPH)61nJyjF@8)8B^a^FZ}FPc97(z_?D5z zSQXh%Be7L-E+^q~7fc1n=asb}UUU$sdMiNsW;PF_hEL2jl^||Vt8#!|Sy&ifi8GUq zgU$#&0{+WGK%Mg_7RS=CP zP*Md2iZz}8f(It6*gR*#L$f0bEw?lL#4^U97^YygW_^=*(aDhatbt2r3lnTGvP8xl zBquEFP9h9`b2^Bo)W2rBJgE_q-~N0!0T~~KpEf)9J9zGo=Ck@y*073EGR@s<%B<&) zCB4bHqO%8Wld3|={}BvE6$U8yYk2qC*-B+%iUIbflw?;sNI1nk?OqO6 ze3VLthmyrgVWs5t)gT(}JX>c}>Qx_ta*&I^i<1n{!9BmCI&fu@})4W13C~u<$>^^#l;mTy|p9ufgf&_ri9P5EXiI4jD6NeO$6NwL?NaFQ0t;hyq}6{n8tb|g=~bFfGlm5R5mmFSK|a`_0}^ko9yZggS* zH?iWg@0i1mr``lPn9f;)-NX*(QRX{J=y>WV{d}F#SPciuf~O`|%yGqB?|E>8cU{8= zU0cf6Vl#g8n=mvj(VPkvbRfRx&^Uhyg^EL|4Y8E~DPJ^@;}$PozmK>N7l%V@o+Ua{ z$92GxAW0MbC+^#DcMqv8@<|b6tcqH%JnXs9$$0Ca3-yA!p5w0E?y3GAag@ce+|%##yv%1||6YYr4a2G+o|Ca=VMxk3n}h%)J5D6(YA>g=T=?W z?AMsf?vp5w=^rNFFSoi`6F={dLvAZ)*t3^IZ3)w->WP9Mp0s$k!B2k{$&9_Oc}C57 zV&JgFC#}ab1|LYA{o>sGetGaQqqtGSS1~AOFq1vY>pq;GQL{+Q zoXy6JY6}M6UNwjP*i3mS#nbNLy<#h*eZ@AlEB}oIeHD1POxYiYZwm-~n^_eQR4J%S z%3evF{wuNPO=@h|)vhvYp;>`A2Cw;Qkv=2un}RnW3M{gHQW^noR|0A4BM`{(HM=NX(K@0&jlMdyQ-0&9xp zYss6Z%(5M;zoM$-i-S6WchT9e#8%*DG_X}*(bK#n==A6Q^n`WMU@5@4M_?lFTxEq; zs_<&jCO;YCesvv+D8W1cs?&*%7p6!RiAlEW7*(O7yn4D?`3ZT}#NQiaIQ)3lE?*q) z1Z<5TNP>)R!Wblh`3!p!w;RIW%vEi~@a_)h6t1>9&pI5((pVG(!B&qkW5T9}tj}Xt zF!=iMz>y@3KLf);O@pPoq<5~xdZrIE-$>=rr^~iw0K^ge)Yf%S*1Ig<5OYfnFPR04 zdzHQcH8M+`J3A9KgcB@u6`gMn*&f7X^K!6mM^GFrbvIey$tkl*$2W zCjQ9O3uv;s^5OV;>E=vH(10!=6t_OGlMOh9{dRWj=sJ4`{tKWXa{jXKGlC{WI21sk zz}fA0yYsn(B8-#%fSWVLS70fRYx(l!YMe_`+}1VD(|Ah!-ni?>N2#fh0_}Sw&>CO1 zLH>HM9H}^`h9<%jSV(clIm9{0UqLE2!6;nd87zX<_3#|>s814 z_v>P6-;ms5I3#{DWEzK#m&;NfQOP)T4XhJ01XjF);%{$tGx>T7YmBOcqb4>OOy#Db zm^ypj_=hN<-f+{~R=DDZFpn!7e|1()LUmlNqRh9aW)a->gL8yzP4^Yg&rds|xhU$6 z2j+jcRtL}MO;82#J}NhVI0lT)_kZl7MBy$*tTU>-?!;k;4N^p0LmoK0vI6Q?yt=8p zbZZwXG2yxxSG?ZfUEU80Xg*_sm5+&W zlO67G{9Bjn9B-a;NA-saj{Km3T{%hyQS1Tm9YN4Cp%n z$&TwuQP^0;hYx!g`~qX|MUY~1fHAbktis{MBL?!lTcXE`2-g&2I){Xe&8a0&ETytt zB)chU+@NiuPd%b=>%AdGSn_+JHW-IgF16tyoLP~z?*;eig(0oQwhxu$qQNNQ0o(W! zdD)FHnsPC=pf`8xJCkp?ZG?3M51CY?H%6>moj6bY^*p_*T@M10)pCS8!nZHz# z_xl^EMdGT=^EFYCfFs`I#S=~4+#8{_5QTRg&Xai}MG(2-)MC1zmCxZvhcJ;ct&-KC zpJ+@tx0Q1~b<1Ci8z-^dW=1^1-r+_Hy|pZ{ia}a{DZ1zWXG(%;9()?q8CU^y@)x$a zF2?rrE6}|q{joWjbQ+(esOklN>}|4!K#$wAG)aiOO$oJHm$_m`U&WE)S3Kky15Y1k$qlLL`*mj;%2J22V;vW@sn(M#dWXisO9 zz)Al?iChz}#_tzvLwKX#y@-eeV6ut+0kKeFm$6cz4IjFeuZ-RbY_}bf>H@!U*Rs-Z z9P8lEU@cyG?{}o^m;hxYWWS^N%K6WJi+kmaa&luK1px8#ehz7fwfT3~f^TcpbP5S7 za8NAmuQ#xDz~F0;?A_r~%su4we6beuOn)gr{Wg(g{g*tI(T#m35ixJaZ3XeeUo&2J zCtQXMG^*qV#qT1Y5?_>vJiY&^)=#x8wT8t;hr8(bc1jbzgkDeX`{| z-U@zC!Q%h*0nMP3JSWI%m`Do<;j^%SB*vY31dTquZi>2GlNmmG_UQK9CQ?m57r!}B z*R_^m0uL9xs2ZiPjG07w?bD!uHk?($4tkNAfX;V5*~`eZ1~`-H@^p4ZZW)K4~tze8_N5+ZIk>k z&7I%XLg?58pk&oMvEa`*2CEYpj6KGPk-1SxU}NAS?@Z<4qdR zxL}jVN?IKID=GIhugg8b%9rY>FEc;96Odd329MaB+0EQc$~aBd1%PiaJjaeTN_^IP z`Ac8t)(^|9J~?n~27H|WHCZ~MBY+eFNizx`9S`(b3q{$QMS^I#IK#^ryO0rmp@8n@ zh)k1y31e}kHtF4D8OT_^*ZzoBd-3__TV}f)=eXXL$L% zhX|-wYmc5*rIs=inpP_8T~rTGrqh>Y0^gu$IMsypz&ht0Gt-+NA;xi5eb(z`dZVCv z-@0^6bc58-x`d6-safU?J8YVLLX@6bR0zr-*4%$4(T+96l6?M;zdwsj9ruPrYu|wvA3+ZmDDQ;km zC$Ml-BsMnTER2ZSZP6cTDW%UGNh+oAU4wGk4dOa2fQK{X83&*coxy~XOnW$Bf@L92 zeV3~lcfYq$Kz5ZF?#*u>1}(5ur;$ZP`J}k+)Hoslm0zz(fF+;CTN3ljT!PMLTm^3s zs$@E@*s(UFM;?xF9M(qw82T1^S#L#GFnV}(@nqd zWQYU@INl0+UVV-DeaIgjURPmp1FzX3E!G?R=KVAJbQnWqto zic{SDho_J;At9Hp+qf|lFg~EQKui}v@!SxIq6~Yu=skvQ);B{Uq;M3SDu)rhk4M$a zbzs$mL8AErtZ6hZ#ccOiEv^Rnj;&CP*m0V8HZqF;%>p2i!;j%i5e4aDqOiw6^=D*L zN}HoM+~7f*o%_P<$JWkx@yBP?_5Jw?eor<^S7IPZF_bw?8Zy9h3;A%P_3OCkL^5CDwetMA?0$q$+mYpv`u4TQo1_&k z=RKDBr;yv|qjPTc`-6L~ihl&k|3D3N?Ea5!IvMYMD8bz+ue@bL5%9h_M5!mv32o)SrHj-{SqVAi0yNX?Em z4&6%{kydvLX>P5mi-_e&Qgx?k?5bHpuX*Pd8RB%_wwtiK?gf)(uoQ?;Z%UffT@{#X zoi*u}Z`rysE|O}7@=6H_)6M+4yv|wZC%@4B1#MvNHjkk5*K<_sme3cdQu7ZYKJJfR zvQPFgsdGZNYy-+#Hgt#W1XV0*ZM-JZ9)3^P!JDS&I~v&pp1CftpGq@;Jy{*@KzAeUvI%Y&%5V)p|C33dgX5%$J!BbMY%nO6FCZQiyjQ?a1Hgr zm1x4z!tLaKT%lyuBx`O-u0bNJ|BI8CuMi= z?S(p_6PpbvXR7`u0|^OuoA}45>~_kUD1?}yA{%8Ps>puF6RJ%ceAy99TJ7IlU9ui? z>7r+VQgz+?c&UK^n&Q7hsJ`;2h5K_%Ri8@n&>f-{fr3e_aXloR5s(H4{LVcTXB9J) zREbf-BM;O07`L!qWQ~__##yQ{46|}~B~M|zK;4cN_JD1zNJc4-%46}Jf%Wy(3dBUj z`uV=dpgR3bCGjOcGu+e1P4!zd9d#Da3zN==6^H*69^joFMHqATog)fr)j^b1hq~Ci zE9K&IXrYW$u)oADSG^ZDTUbi;tY%e$+~ESK=iQ#AaB8wPzP+^$-mU$Yc>xYsV=_3L z+EM|_X~1fNu;ki<(W&|3YI`zXhBOBEGz%D>qGomT7Xo_A*Kq@;-{de&$f04OqfLKQ z>}5#*>r1K_bVM??^ku2~kw2}Pa1dpm&*`piF=127ec`v)ITUvd-hU&D!q#R%R zAc44pQ|dmm{;q6XotuL4@08f}F9`*p7T~$%x3d*XX!*OJ2sBBIPtj@T?R2hQ31&+$ z=UZeJ8wp%J%A=9Yeo%g*&a1=0hsjcr@P6HUSLlBx!rXPRm&Id+6I){aHJ^vJ(AfX9 zIgLCww#rXVw7BYDF9p(p>oBvHS?wrz3X)0`z}7cshKgdIogExh`R@;bQ_NTk zaEGbw8>~p^+)McFW|?cpKZw7j6}YJy7YzDoFmn3-y(itHp7{qA2#)V-%qMj7QMU>U zc8ru=H*}k}!pIZ|zL`p~#(>R5h#0qoC;I`wKDw6vU`Oa%6S_ zkCacC7c|RpWO};I9cqHw-|l^+jm~&v01rRsGGum*pR3vk9^~t z(kA}*=CFU0GczQDSs5OS_n)5AAu0gmCDQJ>0;ALvQ~yw!6=LX0VeaTbUgB(3eZN&_SYEL@9l+k@X&%?>CzKrwkdlQ#RVQmoJ>?J*=-$~^i$1hxHT z>+0Gz0ecX@cYJXdLnu-&Ce?h)NeY;=l!stC7AaNDx{oLp8w_VeWJ>I&lLcq9UW!>z zC~i8K!RvF(WGD(ilt(5BROTz~50+|D_ho_YArpOa56wJTfNq6yx80%lF{K!23<3Qw6@&CMk9}jNeEky#1SF=q!z?d;ma&n>VGi+0w{H2ZoK|njePt3tR=94lw z2*1T4SBA#KN?xe+({1Fu&6u~ql9sv75#tG`yn3@xeo>1ci36yxz_@KUH?b*l2pmKe zVOPkem&_Mmu}PS>jiE4y4Go&3D3b%u%Z(;@iXzpofJ2F<*?n$;7qSO9vR z82rzvK#@QtLJC2dh^yF8xG28I?oErvqW4SWGkr!499tzGtv3{NwBa2fB~hDX8W*=D z&Leu058o1L@ZVomEGyy5hF)LjllN=!xlpdjhxux(HY{fKC8#kfBfaazZ)dPpl^#a0*iLj&Wx%~UEA$mRdoclLX>?6p? z{S935__5-N>eSo0A1vAh@@>wtNom;O#ym%Z-?vl7&fuyMA-8)G@(?nwC))~w=gIn3 zyO(9%3ezqs%Mi+OV*x10bGh$O_4lCxerZ%ZWqrL$+N4Uw(W?3pAzpuVRf9Wfezr4) zViS5!t_%hhf29Gu=TWeBsMn?6!n(lqsK~9Dl)@WLop4HXJpeM zh9W7R1SsX$$h*D$g|(Rh)Y3y@(JL;d@$*!jceEb>+1bH()88C6bGEwRd`nlZemtt~ zZaTdTWo2lveGoSTkF)7+5l>s~qF`{5=5o;~GMbjv{asS$@BK4r)7%P|TGle>rV#TYauv;8|Bv;+X5?zq=@oHr;?f+uxBQmz^o?J%^%d zt3l8yb1&NQ@YqpJva%W6ruJMJErOm)HRObX{|qnEm2*?bj6W-NKFDz$cdZ=FiM?rp z@0By;3&_W>d%c4JWxt705$7Ms{k&4-B`x|GZ`g7Ya$mC3Os*t4yM;Zo7L2w!Zbw?5D$LE1@5(m1TTI^r7(M6y=>4b2C21YQ zqy7mVuDkTb2hE7*^XB-jdi9FlUYGXvH_f*io}u5+}*Wwx-6E)difjg$5*8l zl-+{bcJ+?Q|@ImRdp;j2-6(;AVt3vDS z$Hh~rPw+P;=8!}Y>4Mj_p`i{KdW#6#PD>thYfHVkj+_&Pr{Nd2O2~XLgu#Sx1CR5u z%K8t6rE%jzG8@nc z^8CPd3~lg?YcBo#Q(T$%9l~Q`BzM4v15JwpVt&3^U*(Ea`M|CQq9?Yen+BpZ`9#VI zghH_3gFvn!Z8^*fcpm2}Nx04Edcc9KLfuHCBH%fCW2?*}bT)=9ESHf^zz;iCxFV#j zR}Ij_H!J(rc>MC=UWZyn!dl?d^o2r#U5eXsnkHt#o!L@dE2u^ekkuJ$OWC?2W#QY> z$ia`PmS3-lYREDbV+*c|YTy&6$pQVFKHv)^Pi_$C8~;Ff z-On_etY7(sCt?ko&lzp%oOoV!*=){PXzwQ(beh!WvfY5{KmPD8m*b29f83MbD|&R# za}A#Sh6h>OZsYJf?z{(3dyXvgzdwzZ5HDiG;EP45G5ld z8$YIue)7_sJy+`fUmHfBb08CBkij>t&o^w=rjD14#(Wtt0 zkd5jsbY`lkp^3D;*}gQ>FqCl0RUnW0`|g1=eYXoI5ZnJaj2wk~1! zfYATLy!!lwMh~Z{ZGfS0%+l_Dw^jQ_?YjI!?FPqWaL|NJ#t6$lW248i@8-qg%*3{s z5jrd;7akj%{L*rqx$~s8>Aa39mb5$qpz?a+*EsSJ*?zLMJh45j5(+j#C1 zek94U2)#5CnVRV(%t_Nzjv+y0r&Q8y#w>}F@8IV%vDIR?bO&DFE_7xFE@K@T66;SD z#8WX?A>9a9)j?(~y6T_+Qm6sdQ2jRr|7wSlneZz0@igFltX3Kc_5VCaS8awbx@o42 z^FECnEVn~)lb&BjeCK2ZY27tRSrLO82z#bT=IZPJ_H4F)_r1Nt&hrdKOp4{uJ!c(R zdZ5lkLbPxMYs8Wdz6}~x|NX6KkE<6iVadZ2o^}pGldu21n@%!-&?BoQ@DG8NQXCY0 zTUcDZ`jC)Qsan^j2H$u?n9ByF8tIoy079aGZ#KL6c)Y2y`e7~AXRE0HcAM!)1s(8g zWkPX_{H3&jRQt6>H-I1!9vD+g-p{!R;wZ@u(g{3al>kyRN z@&RD!Dc#^@lDn)nT!wy67wDIB=zWKMs0CI4h%4#D-&wx8q6H%z%0!HxoX? ze5Gy|bo_bXvp2%P>}#Xa0@`ZKxiz_*RkHq z&P{I?lh{X;VBM$Qck4WjNY`D6!7Qj6dYQa%H%Z{UK}3((qVeDs!bC(=tXFZ~899_Fwjy0?z5?@%f2Pbf9C}?Fq zyi-qqboxFv{|1UboiA9cikopy-%8&aYZ~CAnKs^JD)HEbc@zUKq>z#f3G&K4l4b!( z^=a`;=4EHT$34gXptgcTjGwS_O@bMr{Ouw_BqC;V>2qXHlp-+|HR>*1)maKRgB2&} zjM8(aJ)hi$)fLV`)KJjk3|?v~S_y-NyY1(RH%~_%{u?AZsc|7W1l3QoDh&#H&${^2 z{mrv|D5&uz1&~9~Tb75e;o~P)>h15dNEtkQ%LLNF{{Ef+YbT~| z<&F|kU^~&H5jhw{KH-{4)dwY9TJjiF+Kld9{$k-t1o1v?m0E|Beh7Zy=4b9KvCQ9joewBt) zzrBsIN93+pm@;2~D#3Ppa`YM@FVI4}#LnZz+1Ph13_JFrTKe>JWLM~h%W`3~_!+MG{2a&u)?o0E@ zv!T4|c`;%Txz{FuJYj_T^*W{y(bT!J*RidmqjxgF- znl23t+%tba?|bTp9%XX27i>!K*I2MIx3-IGl)Y|jWf(X)&o*F(Mfz7K#^se!>^%40 zpOqFn6y#bC;n;M>kyA!Yw=2SX>J8Y4yeii<+UOYRDm#f)+j_3%Sb7B5We$}ES8e7c z_s&Um6dCs>bj3OGl0&D`6FvkUI1#3>GsrpF-Yc?&Bk-o9LY!Rqp}7TNHlFS7dAfSH z1Rz@35z#kGtF^Dq5$TUAf+~aIS=&Uw6o?~q5BNkAr>sFqouTaDvHW@yIgET(foYwh ziM-mQQXk{i^vKyvES#U>#6QkYbLgLWOg&V5r*T`y5l4+aS0(<$(P4sSe)S25yG!n@ zKV7`3IC;SQ`Tzkn1>1-HK^*T#5SO2jM3F3uY>A1{Si~p5_eHr1dl~V07;9MLAN5@3 z5m3iyzl;i)j{O&J;D%W}z?j4ab8g8}y(uCpZofXwgnT=0oa_)8|(U&(DUV6VSAlVU&IH=QLAN?ao~7 zoQ7)L?NC5A1zZ5g=A{f&r$h7h#UeqlG2>%BbwY(hOO#>;u3v0eor^e{iJL#^JYmog zeZq%-DCRf1Obyy7`0H?flj7SI(?v}F%z~>mmG^K@G!!OX%b@do0u6ARExKb6K6VpvaCyQ{A@r6&ykRF|n z1vI%zLoB`r4iweOHc02COJ1peLZ*S%L%kqkJxy?!!pZQXR%`uFnAmHHu#v z5wmCN&2crsJc9YwQ|;xvkHvj*TaC28i506^7DO4_qDt$gP;Hb*)!N!OOs(?$viBTO zc1YHo*ipjrGhSuyk9x*&jY=xiOt_PIoT(n?0JvqYF%y}eoOAUrmaCI~?y0Ei|4a%A zB!x9ss>KTOF&=LGVy3M|Be?HX0^QG{jrpl%1>QT#ja}hfyHzCKx3zb1J&ERX?Pg+n zXL;?l@XE+;hb2*5r;uo2kN9{?;hl>rS8KR9(M#Xx8vI!*7lH#zzDGmsxWqQ31p2n>+o54Rb1$^ffq>LjyNkJ^7<2%pyRn`plq3xc78F zckfnCu1!<-U*r4uvp}fzGv6CB@6Al6>*gb9Sz*mt@W=Q>0f0j#Kw|DE-89mEdhYYN zu^)=GGbUUfhqm|$BN^RvU8f=GJU8vn5R(R_49t)YUBGfCx~C$c$sPtp3xO~EZKcE5 z?+<}rypSPg3~=704IWTs1)Ys-zXHV(P<^2r6fFx{=(C=ovMs+oIZ9PFKeNUWW7M_V zJRgWNFbnFvH0l?Lzu=p6x+;}w!2szT+ZQzxu)sdK=jtQtb%XGq7ws2a)jqf7gbzKi zZk_rqb_Qz8|N6f`*3uup^{;{%yue9J4^ZPclEnIRzL_(}5g0$sPKt#4K32rPV3OQ( zgOOlGAF` z6O1W;<=jIX2bj%KeHxSqu}~#SKFxmPO6wGz29#>-<4k!iD`>vvon~dDk$!RGb$FKk z!#Do_yY4<*JmF-}gcr}lb|8p;vjXMF(*K*eCMzrW0HCP*t~mKvYGUSe4+29Mx+dR1 zTO)Q-2+vpNygtA7laah4$4o00KHA6lz#t_$#B#9Xa`l%v+5_Ue&a=Afs2ghhz)(mqGWy zgf^$F{!~&Fq}4i-kx%omPR_NqNq$wjNjxV&g8X~e5Y5D_qAV2BLc)-eF!6#nkK^<9 z6!PWlu3blq*^9~#qQ0Z?o0N&pVMp34JH!CVw@@;e8Q2-|k0%B3*_7a|ZEbmlCnr_T zMLca$s(I&VAa@-=Rj-l{i1>m21%hTw5JDKJ_k_b1X^`J(`0_#Rc1-VlVS zKnN5U$Qc%s)G}j0wD?GcDBJ1~aOX~4NA=XK{kZUE2IurN%J`Rr6a{r2KiNxfvQN5s zpZ$DWvSiSxZD+L#W$=E3nztm*i{R7xRtf&bMhJs_#k&lGG=LX(paHx$Q7!F%Z zfO@~e9Imsgo2OJ}cQhvya4IO@m zdQ92mufV_(#zrVCN+OI5)>YbYr5H}}wwk~67A5%%bR~t8?gm!XZ{)oSe>un%#}Pv- zTpTt5Ea#hR)2oNeo@`cTMwa3UB*&Pm$4xuhf`;_D!^oSN0P<8rjtEAjaS$C9zfEHu z9o2i00geMVd#P8g^7NVYa(<%kR+IORiPnqzpYTCS15%qW31ia~CkzQvc82CDFJB|r zA|Bk63y4Iz?l@l`#!ThKHgY+Jx2E(5>vubr@S6QX*r|XW#%j3cAqApE63&ZSjh}MT z1;ZNz7kwKWw=uKk?0z!twmDf6tG3nNyDT2La|~{>>3IOYIjj$R+p2RlU{M5B4Q*_h zWbpfDsXu|n@Jw{^lTz-mKxgSmfS32(Jal_a2k-WZa+5pSL~0tTGyO3Z-kGCAw|@OZ z=p~udSh1z0gHP6`Pbx(}*oun8;u#E+52^ju3V$E@`ke?JpxBvV#0qhfHf@%N&sMZ; zWj2}kKC+Nlh6>&_0pw$lO4M7nEPdjMX>=_--EaCuig-{e5#}9liW+kuFUtA8vV|#0?FQR6;9&=zzOXgiAh6 z?#fcRKk2><` zFEW^K9vYQhwT@LM@QEE2T74%`6u?wk%+j&>*^HI$CA?=tfM4^NiYD6s84?Hz_@M?$ zxoe02Tv8NgM3sSJ=fGGnNuvvngJ}gSnHWFd88x`*xauH$Qk`-N*KIY|ktr|BtjHKW zwbn6=B76Z`Zb43dd1w^Y$vDR}J3ll1qh4DEJ_aA! z9Pxt0F?11TsY|I(L?9a)(lbWC;pR-sv%PazDz49aeaT1Ze^b*3$>47O3n#C4b<2UM zpPN;Q5#2}%K`pDXn0Wc9wH0i?HZ)Tb$UKA(Fdh=Y2OIXQj&<;1Y18f$HH9U)!Cb(D zKfmFID|HQFdI=n%{9w%C`Emc5Z%1%JYj34lDR!mvO^dP3o>H-Nu2sQkYuD;H9UsB! zV_FIWRvVgdwB8BZbDj;Ew6FukS0QonuAu#rB4VyGw#wD*u^ewBiQ8FH=m;l3Hkr>1m-_a20z zp3DQ#GWqfO@i}Z$pufF!%wN9RDt;xWa~!Qm9cp$s?u)_`K=E0zulKhtaj53RfV1#d zi`s`s>9C3d&dLJR`{B3YbL=Z@jRS;W$(owJ_vP42} zC#@}-&t3Ua5wd#rytB4FnipBS-vQ~(f&7k-1p~Y}rY-wV*E{ZftR>^cAHza-zbT3*`rl#U@s>?&MsoMZu|VNCFLxG%v;_ zL+;=9?Gn$JB6BgGPz!gT8yqQD>xq4=J!IVvwRF z84yb{%H$c16$&p1QY6w%-^!S>PmDqK&|q!FNf*_QIP-m%ndYx(4N_E?g?&MYDcY!( z3#}9FRyh2pb)vOh0D)s=lS<^^li|`%JCJo2i#pj2o+uww+NojGXiGLIN(F^B19cN63wdd zY-@jj5WtYzw3QZN2C126{6RbXI!}*I%vDa}BoLw{c8Bii##{Kd%?uCVoJTX}pZ<10 zh33;M-G4h)7&M|3p`khefx z?H_sMgYL@etJCQ4mqt+z>dTb-d9&Nj8kK{NYzg+lurn-|6avi^HCSK#$lK?aS`{{O zM+r7mO_s}UjI0+E0KaE%i>rh58&RpH8f3R-10ywte$m5zQyE5{KFNlA#D1I3;vGlG zsGrf4`+Hf71>p}mE!bSW7~Vzq%R*}F5TqJZx6njgh47PFVp>GQ8Ux3ZJb2Zeq6mZT z{olG0qTsUDtf)~pl27}l(>##Y8^Cn6ar8MRWQq6*#!6^N+8T-=*GC1c_ZclToYl*2 z_N&iu$`<1!&D&I_?s^d%qtZtT3pS^@wIH`_#*k|y*v<5|jY6{icUNHY)cim5H0GNf zE;H}yW?f7IG%C^Wh~F30^AOnZ?uS>EFccr7vMgp=f8?|Untvcl3J5HXFyzP+$MUnm zR{H?Yl6K974u1F|4$M;V&&IuqOdIn&P?}u4!VFYZ5qj z@a%U!eQ$-O_Kf~uO7)2!{X;qXaIsFl*|BuBvxzfknBYSVC-XnPPJ=v=(8|7(c{DOz z>=LX6aGOI2gHl8bw$bo0{uFnsILMy!lY|A38P_RmvQ&GBu2Et|rVv=9Be5mi0OPbJ zuOdzoX#rnt=x=k9*Y=yF10(5Wmx<`t8%557-24&s-Z;Gh^r>A zu8N=i3`T>;`DCG@@NZzXR-*bzG9|VV$sSF;9CI8e-`3h)AsDHFYMsBSPS}!7;Pp5g z(PiEFrEUgRSRy&qz7kqSj@*|K?Xwg&l5T7Iy^Ex{yeS6NPUPS}maj}w|MLqD=jp%P=q5^KAK z$vk5A)9)Jf-0T3)N1@Hw>ctr@%Y|{`hjNB`|Bgk3#_7Ao!FD)R_BC+!xl-j~h1oQe z+I#DlxJ?3}Q+*F^kS{sHU1U#TiqkZ|!32+bt4kC&{jaE)6UN}0%(Pn{%JdIjqYHIU zzj^!oDwnHYX#m?;9ulqd7*j5whxpX2i21AAe~Kl~KX53kl9|%f4Gj2_9AX?H#GPU& z?{|LMWbW&hY~QzmWeem+sJ{gjjST{;&}`V9wv!pR=Yu7&)EoS_@zxr(==ZYm@b?O0 z_})^1EM=)vw}JzcNiRDukJlXyG5Fl<2*)G;MEBfGm5u}VE)JUQ37gd^Hj(wTvY$NtUoEj_NIz>>6a~QI%EM3}i*&*A_S8LVKd=en2SgX(AMQ zpCmGDt%*|>6}T%@JZhJZ^B#xN!27p22}&4ib}&zg@?avpWBo=HgD9r%i& zZ42g9>aU0%y4xY@91SXc_8mf}Sd<+&O#5p*M^_dHFv#3^8&(-fI0xyg5P&M#p3HK6 zp2S|NaaccpOy<;vtXyVoM2`YjO#ww93=8{~d?+%KfbX%g>-P}MFE{4dl}xd2Ym8XC z*7>j-Dhy&?Ps3ftM*qYRJ2~pDvEapsgBAHqWftslBTnaDCL|5bGLh*IDE#-MNYnyS zl!NZ4LfHAXA&dhBj^=M>4IwOf)@2Ahy&9>N;8Z1<Oo_5$MGGf@s zokn?&|Ep#)Wd~~XK2DA!$ZCE`B%64rmg@Yvq!kCdNyK~eydz-r2R0gBlT2dOq|{1& zKYKXySjC(^K~?l&1@>Oa9jcw}el-?D&_y1Cu7~K=>GLgR^WEhJW7=D~T)Y-OzD64Y z)78AzS>_avv`NcybspmBEcBQj6-kESEwD=8;}#ub;fhZS>}|GHd`YUc#r{`*6My7) zT;={Dpd4xgv~QZE!U{L3%>w01^ttC{*7TViqKblr)+4t|a9QI`=E`}-5r#**O_DR5 zhGuAFi-VY(c|XjCy|Eg1!<2OcwD`C)J9oa#fJgKZIKA%T~%SXfHl zMbV!q*e}y(gvPq4WGMFn^sX?r&wVwsF_G3(O~jXo%Z}4gp1Dg9fqd_eaSvF!k>FXo zCBGc0pOM_f*dXw$VMJ|a^+eT zO?ZQM6$rpcz+y?edU2h}VjU8Ja0Ce#%6xy2$rWnUKZlqoSOqCD+gE2NM?WwnVC6M3QOH0q;uwvRc_ zMrx?0wbeOek)s|;PV$UStM&e~R5F4`MZvhFK}T1?t0!8LU6EC_6&n-hIm3@y9Rq!_ zCfcmUe>Egkvavf6!BXHfEQzgX(1muo!Uzm$jAA%&UO)X+HG!1|!bwxyXpqa*QpCp& zs}s~As$A%>A<(peYd2yn>;lF&qYzK)f&oLa%~Cs9c5m$a2hPtOkCoX(BWACE&V;_9 zNL5NryGTJe39p=KWk36X7)k0xKM{xM+>4lA^#^vmd=59cd{Y$j}wsv2j@ z)SUpOLXaa9QXGqcU1AyuNnuHv$$m?l!NLBK(N(d&$|?Q#C9%0US^QH>inH%$G3tm2 z0+R^d08j>>Ymu@u}6j{c9Q> z2ju9szDr=7f)|nlZE&5d#WNZ^WN^)14 zi2g}1BZ58i8kW)Z#8ZCqijOZV8!>yRl5K0#wA=>bLhDF1J@_{Yxkt#A&CJ&jI3<_Z zCG95TH||xm>neb?wzTGoa43{Q-N5-HcJ23D(#U!7o4PfLZ$*Cl7M91>)-?Wu(NsBf z?d}&orMT1>=u}q=T~jxl`Me}1#nQ2D-y z6aBQ_TzUVxTI!d#l)4%=Y_NS!0~8ma*XSr-1G_~Q>6l2>UnEScY2kaE)E4Q z4@Jzo@iRU;=2zu& zbY*QbzY(3Ii4`t=J#yj{M=y61S7B9YNfv{lwOH*PnMkWZYU8swHiAA?Mq_ z6xsd`A7U+_lB6_CwGby}f+@10?Kc&>2ikRcF+guWJ-PQH*vzI-ZLJD@%{1;+Pofoq z!!{IBJajG(zLN8`hiAvEHQGS~XaZ#CE|181Y;Mf_tZouks_#Abm@RhvrS}Ui-Ug2;^@~j)fYZ#X+ zUd*8DDzTaew42ib88nUUX*&6@p{7dSc30(Hcy2v!K5yQGxaDW5C;VVmu2R@WXC`hr z8O)q<9fGBl)`|EaT(8q-F6SKbXJTxJNS2>`M9dDNWbFTLt7Q2*4TVP9#v>LP zyT=RsGxJ@Pw8NnAsE!bER7d5nC{2@_rFriY^qdF<;Y51xCX{OP$MGORQD}8U#IbkK zvb<2z8SCZ^jP4ntdXs+@aH8PJb_r4~IJUX_^%rP~FcW^|3yYKfvFK$(kgbL$%6V+2 z2<}vzm|0)IRD03KHzsIWbeUA`IS!+->3G%Voi7%e6V)s}VMcm2bzD;{E)=rXF|&>% z5?ePNFivclRw|Ldn&a@sngp&Bta+c$>i_VwEnl>R{_pEl#B(H;{lolkmyPSRFuK(u z%fxP{LR3=+(CJ{EowiJPs;HS9nb5gV{J%O7)xo$2fo3yh229@|+Ai?UxYbKEvF^Tq zqZ?;J+x$L-8+kq;C3JP5E1)h$J($cYL{o*MyV_a7KG8n_eK7L>e$_xVw!EZu`;q*U z&-%Aqi}5H#wb)%9?EolX$S-qKj`-sa)l0jC#NvwT1`OEwy&l-$ikREUvqZj}XK=s^ zapk(Om*D+9EVA$q7r7m_=g6wwLki(vkZlB+m2;Lm)JRtSt}Asam#+%LyiWh*=0@bI z{i-*h66|oNnlNLkn;Q08pt%LPiJ+*xo`@4l4oBFe@)rugmV@7?pK0e`S0=B$r9|R^J?{D`EcG=A9S7#lJX!1ZcnyH9i;+QjW zKavZYas4yk0k-a+#?}XAsYrK3WkeLw3nH}`*j^!i2_-io1K3eUT6QaRj|-1lk4c09m;3_K{#~V z^+_yr_MB%mNhzF9T%_ivv-R1>ROyYJo*$yTU`zvNe{EcWI$Vt2dYuWy5 z*q^7E^Zcd|`pQfiw3kWpzOL2?LNw-bcE~Izo3HL+Rj^K5W}Mpf3Vu8VlRd#*n$j3Z zWg!F`Ss`_TTlTnO3DkD6sT?Zvc8yOu>X!pako_X+f1N^O%xB&t_-Gv@)mr=L?g<@@ z2Mx+8%YHMtqhmg~lc)z4UH~RHhJk?=94>u*TxUEEx<=EZUErG7ohBH91qY9awSfKw z1^ltnHkgi2^1RSz8qP@DrFf(Scx_ByiDkd8=NDs6$;2v0M);1t+TNNs2i4nR`RpuU z_+NK=hZ3!+Nyq8HCM0-&eNrv2LUpQ289Lr7sZMiua>Pd^z_Ad;3sB!NHaARb4-)l} zkoAnFftW-n^*gH}`iW%DMRWk!)_pmyBNt=$fs`5Us$TGp5&_HnOi} zl?bQ`uek8|7IC59_FRRLbL2Ly3z$yj)27oI?iFyD!wa?X?+!5bw~g&&pPm^qht7G9 z9#@HNTWJ62LS>Rr)?Ub2e7j&{(v8r|!O~VV%`WgB6H2(+RIRXmy~q`^K$KQPckAx@ zEVxi%H<>N!@SmA3^7R?eSi-Ch42EG6EvAPTQJgBEY&AEpcFm4Au zP`7cQb8GJ>CP6r$W!5IgRSKF?_hRMl=UPyt$K;S znz|~^X@x}g1anaWp^w;EDtcGP8MCDX7F9nA-$82V&?ulVhvDspuR~i3( z0pojRFt(?nq*3#zboLzh!VqIAyrjsb*VJ8L&l8O29~8~L$Y1rraR5;r{%9C+C};i= zoixiRO^(pvCkaC!rMm!qbD{fY$T)~NIa_#*__ScjQZAZrtPJn8My<4KCyG5-~i78Gg*n`Ax?0K5VBdjMFZi-MM0+4QH$&uMPwFeI?m%2bX+) z&95p+f^{m#RqJmF8D{?}Rbt%ZR+z>&vr1Yy=wl-;WsSzGMkM=P4TaMD3#HWKj6bnY|mJoA!^O;EN;QKdNmQd02rB$M7J>Eg7H2zaQ;Z3-hg|t zFzf`mHgCSx|MFoIC2Xl1KJ)J^wCF1D_H|#XMz!J9gDl(q=(U)UdJzD&r4k0OA@llm{2W%VCFu;V&qXreFyUQeB9 zR8PrwRnhy-%Cd-V7$!%OqoR^L!LYQu{zyt1gwC5TXmd{l!hg3m4nH=9phi1F2Y33E z3WnyiRgS!Vft5N^M^+4yJs0~jkOQDEb5#_DEqokSJ z`jJMaTAYbVf=-XMd}ho*rKHvpluJ!36*gZ{9;9125^T@r^ATeI0iNf-aUat>!k#PD2zBP1vwL? zn}^=OG_j7r${{D4++~X6sL`3P^5f*!CI09HOgaJ-Lqi2iBCT z!qNVDL-Ct+#3+7ko@g6x#|+p2Oir-`GpG^Xqs3lvXOIk;^o%37IZ|( z&A^TkgMEak9%X?e>rRE`0I|am4jl8}IfZ{R9NgNM$}168dDKZu4&+q*XA`D+y1 zon*D2lU`3M`nY@4WqV(w_V9)31*V7IW|%q37LWeWld9VPHL?LITKJJRrccjZ#*Mq{ znr8L7Z(dxD94z`T6^mw*?A59X;|hM!f_Z{qrUQPUpI;aN!`(Jzq7HvXX*` z)M718$R)l?_1Fg{1Zl-f@LCcd@K3=Fik;s-Z*W4p@b2`4^(`&QSbeyJ^|A|P&oPx= z;>#=_bV600#@B)HEgAG0T@vA;WMtKn2VXEp8rL=8&3O(PtYdGjJxA0%(FNxT%x^7i1}L2`H+{)4V|+ z{|9=?Qffm&_5?zsBIR$od@-k*%AF`wgV*v1ME@H7RM5p|%>e8ud_UJq8O`VOVtgvx zRgYEqoo2A{`x6`3#x<%j&%jQk3sD*7zPeMbzF=e7LXXdlO=i#+A#0Ylv#~qp&96Eb z&3OUkq(EhryJ@d(+`+BstcZlye~buBa8~AMe5|5$}okj81fb>AwZ^(_kqa7)fDt@w`GWqh=B4adXb^G9Z znjuC%$jN|UK&dK{JpNi5hM$BUZNmJT3~r+uNYnF?NaUz}d|dJej~(z=o)0l9rER*|3JPF+oTa}jT~QT?R7z$vuJo@Je=KCpWhYY{&dj5 z{LA_->NHRDIzNHJp}rt8*IMn6u69Mqtl!c62nj}-LOyH1@q6NSMG#0b|5WNpa4Q$z z3N`Wdv@nhwp4N-}FoUh^AjbDEcU`;lbwr>0c*&A2<{Mgas9abj#g%FNO5#wGf;pYn z&JDswd(oJ2s1dTor(7t*ZRZMTz%ntycp=GlsIm7S6wb?a3vo;nYVwUSMU77zpY5)i zqhpyOwFjI_n;Ufqth=~Qcrf!ITr;GwN9&~YF>BsaO|w39!c6kf?0{?*1Z$BgBZDZ7 zg_IwCSATG*br|1Ww|ioZoJF|RGnY;OHC0-D#^HrkrY(<6roFUpuq0!SFJK?Z5g-^C zU)V6!r%x6M3lROh>gwV&do>pzy?glC8nF&|_@b0>IfrCXFnI8M^L)-aeXkA6?343K zQYF39nwy%3lxXg8-VtVIpjnu{dE{)8r(7&I$;`?82NuDc0I!+#lC3&f8&!pVL~i;_ zU#2uRCcr+NUvjLo^c$MhwHJb(3F1&?__gl_T4jOM$Ab_tAYO2^V(m`UWuN2Z7`zci ze$8`m)s;m1@i|x2jkTFgo?k^`vNT{8L7@0c8kLC&P4=0iUoybcl7nUT%iW(~C_tDh z>8u>8Q}KZz)Z#Le&->?RcQ|i7ZxF9c+(a03rs*!)Ov>jKJEP?IDD4bx;n)V>rXP&L z9?#%RW^3m%+jw9O<3+FaCS3yng7jM^NrNZk2D@B%E^#4U=zt{*YH5-IhtPt4ReaL`{r43XI z7Q&Xq9-!^Jn7M#y9Jp1?z=c`;4tCK^}Uu8y=0e1QUY1{FOR*QKn5A?rVUXUG0X7= zf*EhT8&T?C3Nuepn(e4xJQ<}gt5MdBojTZ3XqSCoyY_86U9Qp|)e5DM<#*5b6trc` z2LI+fwpt^i6Eqxd5g|{pgSPd$BKUSQ$b$+jPmS#td4%uB@?BJbEd^-)@N7H&tCMBx zT^eI|_)fKfG1l1AB1k89x_4jE1gJCvwjrMW>T{1RbhZhTpuQpnt5uhJ*V~But#PBr zw`4+lUnAa>BE*fYLVE$eNMe&adva& z1wil#F=Bzud=Ci)YW?YeI9VnImrQW17qviFMINM><{A7fXqobxf1Lz8re@u!gJm+z(B3e-v-MaqsjCrWQzyzUDTEOcJ$}7DTnjqpI$xAzug4ysibYG8|1RO3;$2^!Q4TdUnI01 zDcWe7Pl(*Gf!Pxc++6&Ri{0T>DifUgqLmvF*;kUs(&idWyA~3KC?r;J-jifjK6l;? z+vfN=&taN~a)P*qoI1{XGJf`T0I#Sryv_;Ks2jkL87(Q>ED?j(C5JB}5ScgDkm0E` zGg7RnBHf|M@PkzN=>J^=dHkPH&3x&I+@1rrw(Nr1;(vsJMXWJympPhUWAAiuU(K`l z)k)cOMP3?;mO5>Cj(n^q9G=S#yQSD3!!3*+>ZUDr%t|U5+`>v?Th3ZSZEedEj8UtE zmV4q4hNcSf&r~Bp>jc~&(?-oYIdBoS)_9yBFG=B!1S9ISuwT{0w z!lABl6M@N4ry`zAyIuyYotpPqQ~CGH)QneV$hyV;lCKCIocd;tn6(UXqs6 zk6PkXp3Gfao&pD$nO1`yz@m}mKqzIP?J8qLt+5c~q zFiIjIPbZfZU*|31hhFo7yIB29Ur1Eft%^!)IA+h1wc#p(1)|oYwdd%s>za}r^R6%g)N zHKCS+^WHOy?#2=~6ffWmMAAWZUrW|TFm~E%w}zULgtu|6Exa#cI{9jF55(#!@TJoF zdtoIn3SzJX>WvbHOMHk5VlZ#47%U8|5GmY894g|G#jG!Iwc`>6P?9;YAD{sJUNb{d ziAjgz-IMbhuBw%uOFnlNJokJIqO}*TM`xXU7muA69{gU*DCYP*Od$q2Q6asjFHIJc zSMYluvgST(edO`Km>B$&b4^6hJC`>uy2FD&^#&u4ZBdj$=#J5SVC`kpv(r1JzoO1( zPF!C8tMMX1*7vxj+Q+OoYzhUhXAG%A*>pkvbiu?FXM@N`=0w+ZBeCo)u14gGUzY4` z6h`>z4LH1f_eFi%We-UVJvd=6$vR zZ4Kq)*^@XgyoaV%M^>NCfI9csy0WVrH|_ya$3;;TCq)_BN{jrTlfs~8>Pwq?W}%B^ zP&JcQw!yiFZ6=d%$elK`wofey5KnyV{MVSmmYC=PfDZ)-B}`{xucD+Mr)Tt}ER1TR z9j9^bs9(c=(`*uHKb)D#b(2@M!En@vD@X&(&NTAqHr?feo zG>iz7b+pT>_zF1vQ1z_YJi$DNZRJ6&F`Ck~f^qRMP+7rTizXv*f z+xU9p{aw(&jx+y0K`SLU$-xF(m)21W;K44 z_{8HVpfs=Io7A_e@>?q!3%sA54L?_L*nAVQ!E%r47#+M(0KaT+Z-4ZO2qGf^zp*L< zJ;_ROY&fCnf0gIC48j!$`d&mY!ykNnW(H8{_;^RZcgihj85%qNngj!+Y&Hu#KM4d+ zd|Myf3sYZ;`ouPQ4Ju2K?zGEyg6*Ft@A|RaKFPC|TUodRoq!J1csf&EEe{*d_Z#+w zW!STeA$6uNaR%JWgw3GfaxhN!SGi)@|i-~t@Va(Ykh3Q&nPmTw-S`sRqpkA)rxU&Jug9av>G61tQ1W_I#N z8wcvR%K;{q_v23waucMVN|@UF&({+!HF>My0%vw@-Pyk0qnC2iZ-Dj~lnBdY^dBTf z!R8n(r7Zb4Yv`vC9Y%~Q)q8U=a%DQM@vS=+Eo`A7hGE=+IPa{j4H804=0>y|o8SOqm)#^(Wau z@{>pO3D}!Q8!n3vsKeoTOgfC**G_&eN;NXq3VRI&u5b0<2_7Dgv+GFdU$#@nk)oTf zDf6G9zq3k3Ow-s|O6m)ac^h`gt0aX( z$8?R~Xrm%5`6}qx1exL^`JjefXB>HM$qdW}e8NTF-557ua7QQo*J+&i!5x33W_e;> zL;qRqCfkfL%9)AK=jMx`ceDu_C>cP?)z~rs!zKPwJJAXLw*BrNjpr?4oCZ|Kj!9*U zs{9#IY=xQA9OBDl?_4~_V^4fA0O-OZbWuaF{tA^;$Wn~yU0gAFUs2|2KU*c^E>CCZ zB+8hsd6evZSD36vO(U!`{r4a_T=}@Evl{{c)nTh+FSMJTnxW&Sqk}nDSMWeH)zkIe zg3`mipU2#KP4yc&;Iabn8pYU=8jAtB=~u5C^a7Z?>fO}BB(KzoFiP2&U^C*5I07sW zIoKG8txZQfvh;&lHwa;Kat9asJwP!gUk=x(x~$>?-~W6WKBz6pna?mDN&k%PW?BcQ zQ6D))cHq2`gm$3e`-2J>(IP_|Q6@Cxm;uWCgx|-eSBiSNWw8cx8KRx_Djg9x0X?RX z>XS-ZP^W#Bw|9Pl`bFLpQIq0hT5~kL7P}IDD7gf*2$%9IDpa0mOUi62w0WgmOKZYy z#;)Zf^}zz0z@8VCCoF+Krd#wa&^ zo2X?7IIHZ|kRd!)H#ITm3eyXIGYA>n*Fy;&&=C5pvZg@2Ewn&k)!gM6o!yb`;&MaJ za&Nrn#YUS!T8qM|JXecoNV$a@IvYia#&L=u?mOy1@YfhfzW%uWCKrV-ouWKScv)^J z#OQU}T-7{oqgv$D6X7{U`B6&+_JvEc)>|2{=POC$ymE2OZTpqdj2g280(H6eL(b6J zS2G)k0_MaXeZrZ+u#VK2--4Tvg@Sp;(R9n!BQeN*ag>N15MkFao1?{Qh`To|Tk~&H zrCg)pC2B-zo~JfB;#P7L;2m;R#r+Yjw%woV*Yw3K@7QY)y|PiC_C!M zKtDc!U*vkFq#hIP*#1xXKA`qIG5Y!RyAJN%;4$BlQ#QDP8}$4EF{ChyltPPHd<29? z5TSD=2az#Cbw}up6Y@=e+_%>*EWRhTL!Sm{y@J;+VQGrn++D%sO5Vq21?-%r5cBJ# zEiPFt1ydEJVKyDwD#@TT5V5H=?SF3iV;4)?+=uiKvEq1s*WZ}{AVTwCo%V5dX)f?# zq>m7?*FEGHVGy+Y|JU4C2i4Uq`{M5I?!n#NA-GF$cXta88+W&$L4$h;5Zr>h!v;d| z;P7_7BXJt`_@dJeTiV)!Dz2-3Z{WNg^t6Mkp_~f&qk;|wWnJRvGx!-3KIU=k9cO6+v+5~ye_L4M zY+D_yT7EEW%8$$S#q;C&KwY4>I6b2@D%2=x;f=g@%Rr^Un7Rt8dp;{y+8qv9hqm$@ zPl0wvCiwa|+*`{5Fx}30)cua0{+-ubo=qJK9xnM^ypqTHiLFZnq_=O2|BQ(b{Mgde zU(tVp<&7EJ+^0k}zW!`V7DbiAmlwd2dOSL!;$TqT);&iPh0N^}{pIFyr?V)(TB+I7 zX&oY<*B&deKE<etTw6^c3?3hL;3Y8(MZ?*q}2TyKmZ^0B1_D@v^fe|2i6PNa?h> zL9uag{xjAPCfwdY?1ft2km%x0;L(d$!GKqGd`A&T zhy*n1s7)SO`wm0P8^wfN+m5z$RMMYi$!XqEpK7C8iVC$&d$QATqcmHc#=scCN zTXiyryTWx!6tS?>G_3cuD@iyv2!cXS8WVigEy zWN68S>b7Q%3;fXU`3`SyjhP~#T+Z_Pa1~_j26#B$0eKhs)Z_bdQ|mu^Ar+W27ydd3 zlxAs zIkmzsdb=x$&o8VwnszyXDmdoJW0bo^R8zeR6ki3#f01yJ@XCE8tS7N__c`Eo&-eX1 z6~Mk1>f6QaRZf}NHthKzbr`Fv98W4OGt`6WZhz~TiR{4sM)Y+a#Ef@Nk`D04hR8Y&u<03>!OK;j1 zu|r6A4!J+xGWl(@aEU-Pu7yAb*W|^rM^$S78%<3Oq@rEF#gHNoMbaVnUhq+PxMk4| zINU^>p9+-X?(BtA382k^BYFni<}eP}cNYPPI3+$Ojz4$`QPeW6^Kh9In5sIP(Kq6W z1K9FtD^34&pN0Bqfp-$cYV4bPvvx^;0{wBG!gc`A_24TWyjrY+d)!^7aN>c55)uU- ztW8lQ*hxL_fu`=lpbsqAbm>ymq7QSU?#S3Z_H4+;mlMol@1bb}RfHF2<3PKH2Fs>K zG+Wn($;zogmvMIXcXkEr(4dIQa@Z1we~bdVy*iiIMLZfeL-UdaLTkJtOABIJ*Isf6 z+*mon25pE=q=%p^qt8#R$4wf*Et-DLFT~UuNaDo`Mtl>xA(pOaA=FTqtDA|HF2875 z;$xKYX3+;{DW7%oO5|l`=X_cZFYs!#(ISCZi2e@%NPf%3!}qWw|AL*>Dqn?Zo`p^f z8;s*+UFF!uU*>l8^=MEQ%Yg^pRZ5q^d9O%wAivUt=7{V?ZT8mihc1!fL zto&}-{o=Wjl$%hvKr7@J45dDLZ0@*}1kWC4bSGQY$2LM6>{FBLX`g@OEc2=Ifc;mk z`)J=eibuWrgdeaOL2iiUUys4C$@6odXHUHm7_A?k zkrL0Rs4SFlL3AIgYaI;c^a=Tj7hq*Y`&SG(3^}7_*Dm}S;I7y~7+U~XWc;^MtV~r^ z*UwZc>9et5DBr%J!M7-0*hPIqib&;|){8>uV)kQ&0*H?6E54TwnaQi%n*b0Nyq^N1 zP)OQ==NVmqeC#8-W#X+frsLo)>)tHGf^_>Nhn6C;NP4HB)9$nXS{>~%VvQ81DIj`M z?%*Eff~+if#yS+oT$ttAt%7m|V7Z$4>8+`>dg5@TsHkC=+7n&&cH z46pSeJhR#{uPGNwq?C^tb8OZ8N@~5CAhN6Lrg98UeD~3d2^~{0!zdaWZFn z09}*m%!!&O^UsusD#sQSyj{e(i`MDlAS7Z9whOHz%hHC-(x~y{<@Ci_@5_F{{csPq zV!0_Pr3!rqI{w^Y(L4QsXE1Y@8FPG-9>^aneg_JH@+oV@DuTR)!R3$3d_g6iFI!m? zJY(-IAD6E;eNSpkUGRkd_=_a_s%XH=2yMal?Hh74s#MCC^KN6F5y_DIT)Ec*5fLR8 zZgMK5uqy-HbL>iI#e9m(j~dB>0k&bl5O8;wV~vz{qLjUfFRa-LtwyX-H4V{$cVj_S zA{MS|D^}g`l>v1*Mf%ahgTPd+ij!ZH&IH@PX0c#?{jsh`&%LK@8t{1q1i~wM*ouvw zewGay1hdj%&eG#NRDQLO(71nx#tcNHLsDj(I!{|n-M<4TZ-ox;wkGv9 zP|lHgzmcaI30=)u3*pwoBML2XrlqAR=#w17R1%^W>w^K+cO2@j5Y>rb@)rtownXm`6!G?3--z!Bk?z9~~cRedS=0rt$G7Vn_Bd0>3{Jm`HJx9P_J>_)or8 z#Fu(CTOp=_!jq*SG#N?Z<=|YylKQg-iT|y|1HS8ku(kFLZZj!|EshB|jh6da1j>1)-P_@JkmQ&)^CrVT=SU@rwU$yB?;b=J%xYPf{xD}s~*pKUq|26 zb%E~J0bL1ka8lR%(vK=~Z6-aE;7BvN#TY0h9cDp(#4U_b)(%>Q$a;2jdNOx`!10+P z%mS~mmI~uOr#NM!kZgtjXm7K;kJ8 z>`cU`?7C!mqwr@Dk>}aXdCMb4ND3L;SM#!H{ye2ar{3qEps}{L{UG~xGe~4p^)Fkp zJlIbVHKhmQo|;?>jI^#eX&iGHB;@kHzkvD|Y{-+i15Y{m#0}N#*jL2eGyN zACAfm>V|=EKgvJONuz(yUPcN}xFehs5_6M^t+m^PrVgRaQbK3;uJy5^%~kE1ydgRK zzA%kan&#UtBY$@%C~EcTZjJ=a>Fw^R4cHL0|0sO-O*4A9>aonE1AkI>nH%DU?l?4N z*z|=b)iQf5$NnRXH_iT}z?u1@5$GZg(2^zS+g+o8oB0f*z)Dh5(8h|eHz=~6zxv7W>Pm%_N*JjazqjI){A!d?lbSy=^7Ws#I#pPre>y0eFFy%N?>+Q3( zs*%QL(#Gz6O_AP+3IlnBHT1qFA_Ys?A_u#6(ilvxZ(QS7j9e z9=Vw%`ZE!hjoq`Cf^Ik1$~}bLJDRbDPR94)bXs~*Y25F(U?j;g5MyN_V(2JhbaX1| zOtEHG#N|w&n5cTt90Ek6U@oHL5-n4h2f2ldp>VIvQ2oOXH@5jV0bkb65{B2tfL)VW zSD?)#5&?m`mi8*R+|Lr06qTmR$_f6}6%};M%wMY=Dbq%DzL*C)<}sXb3JY(1`vxJA zG6^jT%dSRa3BI49cJ%%GcXCJ*@%}`bQt6vOO27ka?I(?L<*9-|f^jN64UxXjEcnU0 zJhjJDNI+w+rrJ~4)M#i!!Sy7d#Yyz>p&@U|G))!)yI(s%W@qj4eifDUb?Nq?Vud+a zB3?Ly!jxPIm|G$;vKz0hVq~;=eeE6)5MW_JUA}iFy}P`$q;uKS)iwUYU7ZD>R)hgA zRANwg-)?P@8KxEA-}|f?_b8PdAKU&MNu0U5yu@p{uTegw(8Lh3E0|mjv7YHbZHgQ- z3XGqRV4{Oxa!J0xpL>puTU8RxmI@G34fy^Ek=2;wix_p9Jkn1;o7~MX>)hmf;X5{; zTJ~e{qM`vRtDvBQ)!~-9hK2?nrw#u!>AbwWfAf;eg0v?kC54EH2*^MR#H(cmyS=?l zDa}OwdSk_GhJ}RV3v0lpc6oI>xsb9Y?4A|S0|H1W!nM>|yF!7K(Xj4i{A=Sn91LGN zU&a}DIn|v$^zIR3JNn2F8NR3Oux)N`=HsWm+#`pnsjJJ#$f&EWbzOD&N|~ESv%a(> zD=y6ql==CU8($$4X}U(;B3w^jzX7G=^xHSJW2eTM>1k(gZ@dmbl5lfuFXz+~`SoZI z%Z{WC254BjJsfE`Oe{%O5F!DaHi&(*h`RC>i#%IVG@%y`>qW5I+nK5Mn@X?~MleYc zDnD#7^=sqhM{~y+D)!6r*l%QM$O621#qX%R`@kd0KmWYB@zB-H>dN2+_tVb#+fBvW z+uPbUVLp#DlW0B;4mGRHbabDm3FRIJH#RmZ*5X~_h#kkk%}9cE3`9#$?&AxiXb*8>;1XyFTEiE-JkVw77iZ_s)OQOPD=bkQ zVM$b0RUK;*+zX_jwRCpo#O37V+@5W&s#vn8v*H)(m&?ZoXXbI5qQA~eQu$?NW;%z* zISXZs10Lp>xwyEH&Q`mBif&d*j0%FYW_YE|g>4PzUOrwX*f|Wcge;UO z7?qa7omNa)Q&ikVfX+6%vZVQ*MvW2f?!G)d_`8 zzXK1oP`zuH)dyxE&+a%}X;iC}Ro>QO@jp$Br? zXL}jc-*Wcao8WNOq@|^Sy;1Kalk2G+x5XrLE3SBr3=GCcMjEu6{@VOVM`x$P{qx)$ z*2(asZBKJEbRj7zX=4p88HGuSYWoKx|D=s&z1$fQo`ufKVgkj;Coq$68a7Vfgwn10hST!teGJAU%lb*5}<3d`9xv7 zXyAiu6EOGT+2^+-c+L3C$Dtxvj6Tp{0l$~DyTx=kJ+omP;$$OajND6Ni23>YGAJFg zg@+~28(OKWRH>dmSPqk{rkSyd1SePmVF&4ayzd?l})EvfMzw*=e7%%tH#avwpN28r?zFnPY9BQ@H8s*jC5is(c3N?!gk{iNX@0OYGoc)kj2whvYpOS^aJZ7#=gF zDg=e!z1P#05A&%|r`z9wU`#ostM)FY?EctYEl=B9SN1>*X%Z;p*kAn52vhEFlDth1qnn3khIy zTz_Zm?s6gG=LMK$MUymX8+UO{VOQXBAaMX-$*m4Yp}hQfNoxZ5YMa)51Z zwy?a;78_VsU{wHq^66%&5Ab64MlLmp%TFz8emiCqN4EHbpLq6M+@A3rHe*Nl9ZN9D zB3cjDDpehzdnh1E)uP&0%d3k8<%iGSXOJ(>n7CX*%Kp9H~aDu0! z-gm5+mvYueQ%#CEK_(pLTB}j=BX~FM{Ie2+a!UIM6Y~aE;ZQP3Cn6&lY+Wc0i)tvJ z0R+*B<+Tm#U=P9j1n8^V4!@|59~b>l%Y$gU50#Aathf{UQJTmG3*g--JJKP&4ZP!V zAu@?Kvvp*3N;p3;4QCJvJltKTUkh+9hnkp3Rv7ggakH}OC{M5_TANw(5RV>hcQlsI zZatA4M4h-4nh2)-)Yfl4fDm@Zn3Rc-Xq}ZYRn&Rpy__had6=)YO$dH~`Sp=Dm|2r- zOqv~iL`>Gm%bcY5^xM{%dN*Y7pC-5d?nJXg>Z$Wy?4rBa`j^zEIg0d?pg7!=@hJ$l z$?CZk@dtQLO~9|8J)-#pNYcEAxUz8?Ie&aa$Z}hlzvGnxS_-ySmN}r~;|?2C9CY9} zHdi~@MWaN9BGya?Z`h_=a~0lx=s1yUUOgodZeDlf$%*@N?;qHU-v{B$+K{D6=ACr{ z`Sgrqy^&AS>&22?RDA})m52~wY`y(<;&?SisuiXM=QOE2bPfhidW1Ep7zaM*y}jD` zjEkBW*Ud=uupd?6oQ`gLg8SwEerWgAeZw*1?p^vIRam_xw>|k2xcvYap}jUlNH~TG zj^tZA#paSz2o5R&4CJzCQ$++9W@h)kFYxo(XzV4jtd2T!q#jZLhg>PYoM{w~kzbKcQ!&4UuRMKc?uo%^1 ziLuoEzcCRzg8At{v%T#t5TpXOcq|4L&EPpf(6{-40 zed&a%)f1RAb>*QInj?)wza224w1PU8dd{>u&P&zd=rL3%cj78q{utifFrBY37JS97O6P<$SMxKSyLm*^A=RdBVUAJb*H$Q93zHE%3U;v;Kv$+ z3?ndzFU`NASX0Oi#LMAb^g_!+HEIcWi}qAl-=Ja~GZQcMR|%S@qgH?ej>?rvj}zr( zXBS_1_li>EL1kD0-f4(a$qL1kqVevnzA=wd0xMiR+8&KTT$^h1*D1Bl;mbo+ZwdEH zz9HnnuHL*SH~^jgWh-KBJ*S%3WR)Mmfaq{lt3>CJfiJas2FR!Ja8qa@tfNc;_kSEp z9;?db;RYM}L;#^6e0}oiGV)i!7*R+OKhm^dSwif&lUV3WdsnH~I_H*u=Htol)lW^P zhH~HkpeB`_lhdD;kL`V=HXy&smM5g8ERylQ@W?Q4smFv}7*~h+eO%1E!xG%^!|elt1JW2gnvaM@x^)@nV_Ey5YF=BIx;WJY6N7C9v*QA#qn|-&6i_urb3%o2L#D~ipX?;%$Q<7QwNU?;xKG# zh?A_>H&1}7+zkb=#UD9vG4~NM8X54}r^@9A3-TXH&GAYqU#4D147MciLQ5ffA<<4K zu@%6`C=>Y{%UC^nLoZ3$fi!4*R@DRQAy-Ef*57C4b8v8Pb>)unmWP%~8pZzUdXy{w z;jv_pZagco+Pt|6qX|tY!yk?ON-RI5m6I&v(lu9ckvyvjRrT8G?R7JNTUVsQ;^@$vMgz$=<2rT_B1L9B(6l8 zIPKM0h?2(lH`-x;q-8ssETfz+nANs-JpWO%zX`pN+WM{H^K(#_)OJ_b6!0#MU6S0@ z84b;x51hIGgvcsvNz5|}$K1T&6dK4V&Pz{15EdPNk#P0im(OK^cH|923FHs`kVzl( zdq{74*9?trh{I}adY`>I=s)Ak-U#cmA{2gT*y+Bsh6$Q`^-E^f#J9Odv~?7X^hXuc5x&jv|Zi%S2&$9}Px53@4n- zMBud;*?sctI)6q!I+yw8Q+9seb~ zE5l@)OCRlZKsYMTN1-ck{f-VMjs?05{GmdOWRuX)FlP)#IFd_3Ol0QbsKlrX=k~L} zV$cY0SPr$TkKO8onX2tSruNH^%%yXeg))MK;18{Huh^ZjUm&vIm`2+5t*-n*NRl#n zf)8S$Zx)Q-EK1Z0stt01mClr?&=xB(uGn8-6bC_V#w&ON zPt4z>wR?Pz^IA8Rrgiltv(-^&X5fT#`b0<54^!zXPF%7QgF=Z-FIe;b#k=7&* zVy8kMetU(qmD;cFB+p(sA_$%m#TOl=^K^ihv&~JC1nP&GY^dw%Ry(GPdqF5N&X*rk zfdsvp!`LJmBa!83cb@!`Zl?!-I*6X_c^M`#lXK-(w$w2F_0nn!$rV|xE}NX8aB&hd ze2nc04c!X)9C+qb<9{1oURFl>TEEGlLb9t8Y3-=5@>jJcZR|DTkLp!30jv1WBV#OO zu737WYWrl2T^oFgp@TXCP}gWkZ@oM9R~2_$7c2*Ae3U-TdNZ`L!paT8*>pCoh}TrB}35^5KI)H4mlqE`-W zj0@2&8fkbXv5JzgE5Z$@15;Wp`ljp-)Dh;tN|RSS`z9=QmrG{1WSCu~g@o#lGE$~G zoe1|vM>iQ%OvYjKSHFIClC(we>#fTv62N$Owmnmty)2#{ke@314YdLt__|yv4eo4Y z-8Dpa99lN@@WTa@?12V)AMfNeb|8DrL_1`)GUyD0%(Bb_*i?vDaQt z?qxk`9hv4$L*Qhdz*`B#=znu&E6$dO^8tRgq+9in4irrA#D`9(4mIV=aU#CpP(ttl zYmqK^KnW_p4z84!CIOXHeJ)dtvUm?(3yy5}l?5bI%;>OuNay2 zfIKldiQ!mKP@t?o3i|K`x}{I1xLt*K2Rg|E&Q%3f60ylY!!(s5ye${$IH<-3yR0@-Qcv@6Iiwak~gYI zL^>R#%(e(rT_bvYfwL%VXS-)ZrpY=+Xy|&l6}tVIlVo*XaUw-fMJE2d9zt_d(|{o- z7uVobR@lMPM^;_FBam1Bh3!B%p@>z;A}Ioen1AlN8z7RJkJ!%-7Lykm%*IOKX?u<( zf)t%F{WJ;VzCcy2s8e7JP9Z%Be$G?33{l7d5k5vsbU4%9YhZwt|MmMy^P6=g&`mBw zI{T99zkw47-EddGtMmH4)G(k)_E@TrAxm{Cm^zt2X)YmNF>%MQfshCJBhmNHB-k>L z3aDhA6$EYDN?eFZFqAr#B~7S3Nq{C(EypX?vSnrx`6aMea8&1hB%uvw+r|WX{!E>GIlb+0{*&j~vX&jdN$ z1)Y504zUK$4f*MNKPIFn%mokP^hGS>G9x+Ih5KjKhr~1(Sfp6Y$au&D>*Zb{Wp@c` zFOtm%hoy;EU` zA-<7$Kz$ICTjGKER>+!5e40||urrMwU^RsXL?Z2oG*gmXY}71XK6C?2DZlKz|)k0$I9jBkv6!9(N2&ceR&iJcSFM^u?u3{;BEOE!bs zVIAOm6F{#)E~6hG$Z%iQMXfJod692SMg~8EZxV?-Ay0>Hnf;*6_JT1KdNwf0!&yO^ z0lx;tc4j=xW&FqDw3QgRen1@47v|S9%67j|jHFyOBudvc(5}YRu|Xo((-2P*AvN@i z6Z;V}=HtNk3+z!BzM)N9!Jsx0y>eBXVk3?ZqX{Lu5|jn+77FD)TOw7x>IyUE%P_NO zfUNjuFa$w22W>T$q3dBZ>}lAene1$v!fh^P2QeACBH2&i^ss&Vl?<^4|I&cfw7JS% zF$>i9b(Gp>NQzbpmpX{RQ<8vmP5_=^q}sD&Op`sQtx`M5&i)5{&UPZZ6r6waMgBnv zE?u0|*eGnCN6Vhdes*(7FbD*e{@4>p#~t->1M$M5GJH zS`ja5a@Q_uXggxaQA2R-0*@v%wDi+}aZf{odtrjE>q`@GA}f4PclUWXlK+Ja5B;jk z99;=wglO{fRrl2-&sgmD;9PT2+BVnhD%nN@|JSxxKi}=m%^9=f+sE6i{W%Q{E#U@v zkk=Od)yHBjzVek#^L+Ru@4^U7hE$6jTgSZgYH(yn#Xf}b)TZz!j^2!byN_bTq+E$` zSY%UgjteB*571k6TFuGQjk{)3bsVYF{{fhDX*DPryX!D4(nv7bfN|f>^#jxL*>>uPpFj1mh+FV#iHvd>fmDaq2|GdYbdbtaz=u zlMkYq``a1nxvb}(G<_PE_oU}fJhZ4rvti3~b=p;G|IU1lN7(RqF|rx2@ZhfZ_#X+} zNNA8O*-0AmINudA*`ReDT1|bv_U%^eHe(y}YGoaxG$7FziM|~Wt`x&hQz#9GA8uDFoN5e>wL%XP45pq(KVbA{Ee4C&=K}DVKxA>=SM#4^0 zPlA~*Er%WSqe$;@0%bO39%bR~-aUz2w_ag_^hL#eG@<=YWf@EU z+X6wohvHsR`@nUEK|(vDj(WEr z`+Ybcdvj92lgvB&SgJD`G-Yjdwd$E&5L)xCJdWz6?fBm3R;$aj%l)afK6BTmEG7E( z>>F<(H5yPFdT7&_rdHaE{T}%T>e+{>vr_blq)GJYEC1JbJq>E_Wqu5m931XWmNF$;O}T!RN0dBbu11FyGiciVE}@`VuV%)YN=~gtYd1sdBNq`$ zfc)=boMp2o4mLJOJ-_QvhwujPw*6Q}+o6gkmr~`9+QQ)U2u0ocwqiyk)F&pf>ww%U z%ap9ZGMyNvAej=XJY8W$yoL!SlkPA46!BUUbl0B|d*#-;YRH_iOb++{-v0+E{&I-u zP|16vW*tW964k(brSj)M5s^jPX3n?ksJ|HX)~M$;!IM@^1s&VbEtzp9|Ham>Y9D`4 z>Eg-Fh3*Ifb?r-CiF>wqkHcPM^{b&xv-J)TMLy{9&kK`R2f;GJi<#E{|EK|6J!6P{!d*0>*cSe{ih87KQ=4P z^F-%YZy6`{-f((iNrAmb0sAMxfs}*{_&WvvQWQ}7-(LR542a+kjyv1qx1BD(3YC*m LlB^Xse*eD!&Yt?x literal 0 HcmV?d00001