> >> queue all the takes automatically.
> > node and everything will take care of itself.
> >>> I' ll give it a try as well ! Thanks guys !
> >>>> he didn't wanted to do that.
> >>>> one to override settings on other nodes, like lights as well.
> >>>>>> affect the settings on the other nodes.


In case parent-child scripts is the prefered option, how do i actually use it?
open director help, click the contents tab, then director scripting essentials.
properties that are readily available through direct dot syntax via lingo.


Medical Assistants are multi-skilled allied health professionals who perform a wide range of administrative and clinical roles requiring skill, dedication, and loyalty. They are central figures in promoting and maintaining cooperative and successful relationships between patients and physicians. Medical Assistants are vital members of the health care delivery system and make a substantial contribution to the quality of healthcare in the United States. According to the United States Bureau of Labor Statistics, Medical Assisting continues to be one of the fastest growing occupations.
As Program Chair for Medical Assisting here at Keiser University Pembroke Pines campus it is truly an honor to dedicate this week to the success stories of individuals who were hired by doctors after they had completed their three hundred and twenty hours of externship.
I hope that with continued effort more students will follow in a similar career path and join this list. As we celebrate Medical Assisting recognition week, I would like to take the opportunity to thank all members of the faculty and staff at the Keiser University Pembroke Pines campus for their contributions made to the success of these students.


Who can you trust? Twentieth Century Fox Home Entertainment presents a suspense-driven spy drama RED SPARROW available on Digital and Movies Anywhere today. It comes out tomorrow, May 22nd on Blu-ray!
In this international spy thriller, Academy Award® Winner Jennifer Lawrence is Dominika, a former ballerina forced to enter Sparrow School, a secret government program that thrusts her into a treacherous espionage game between Russia and the CIA. She emerges trained as a lethal agent, but is trapped in a world she desperately wants to escape. Red Sparrow "twists and turns until the shocking conclusion" (Andrew Freund, Dish Nation, FOX-TV) – you will not see coming.
Three Zero and HBO Licensing & Retail bring you the highly-accurate, realistic likeness to Cersei Lannister in a beautifully crafted 16th scale collectible action figure.
This carefully designed collectible, from the acclaimed Emmy award-winning HBO series, will be available for pre-order on May 23, 2018. The Cersei Lannister 1/6th Scale Collectible Figuredepicts Cersei in the iconic black dress she wore during her coronation as Queen of the Seven Kingdoms in the season six finale. Each figure is intricately detailed with a crown, GemFire ring, armor, and exchangeable hands.
One of the most thrilling films of all time has to be Die-Hard. From memorable lines to iconic moments you will always sit down to watch John McClane save his wife and others at Nakatomi Corporation building. Now you can watch it with the 30th Anniversary Edition on Blu-ray!
Kevin Hart is everywhere. How does he have all that energy? Well wonder no more, as the new "Give your Head a MTN DEW KICKSTART" campaign ventures inside the head of Kevin Hart – literally. Fresh off the announcement that Hart will be starring in the remake of 'Uptown Saturday Night', the commercials mark the first time we see Kevin, the face of the brand, in a DEW spot.
In the three :15 spots, Kevin showcases how he pumps himself up with a refreshing boost of Mtn Dew Kickstart, and – spoiler alert – it involves a heavy duty, green motorcycle and WWE Hall of Fame Legend, "Mean" Gene Okerlund. As if getting a peek into the inner workings of Kevin's head isn't already cool enough – Mtn Dew Kickstart is now available in 11 awesome flavors, providing a great-tasting, refreshing boost.
The Royal Wedding is happening this weekend on May 19th. Here in the states some of us will be watching it online, on TV or on social media. As you prepare to celebrate with your friends why not have some fun Crown Royal cocktails at your party.
Here are few that you can enjoy responsibly with your friends.
Directions: Place Crown Royal Deluxe, honey and grapefruit juice in a shaker tin. Add ice and shake. Strain into an ice-filled rocks glass.
Directions: Add Crown Royal Vanilla, cold brew and simple syrup to an ice-filled highball glass and stir.
Directions: Add Crown Royal XO and maple syrup to a rocks glass. Add ice and stir.
If you are looking for something lighter why not have some English-inspired tea and coffee set-ups from Keurig that can be created last-minute and are perfect for guests to self-serve.
1. Serve coffee and tea in a fun mug. We're certain it will taste better this way.
2. Pair donuts with coffee. Bonus points if you create a donut pyramid.
3. Ditch the spoon for wedding-themed coffee & tea stirrers.
4. Add some royal flair to pastries with British toothpick flags.
Make it easy to serve both coffee and tea. We're partial to the newest coffee maker from Keurig, the K-Elite Single Serve Coffee Maker: where bold style meets bold flavor. You can can visit Keurig.com for savings, including 15% off Pods.
Budweiser are putting the spotlight on NBA players' moms for Mother's Day. Budweiser recognizes that behind almost every amazing NBA player, sitting front row at every game, there exists the real MVP: their mom. We wanted to share a new Budweiser video created just for this occasion called "Mother's Day Tribute: This Bud's For The Real MVPs" that will air this weekend on TV. The video features Kevin Durant's mother, Wanda, delivering a voice-over of motherly advice juxtaposed by the on-court action.
Fans can bring home Black Panther now on Digitally in HD and 4K Ultra HD and Movies Anywhere today and on 4K Ultra HD™, Blu-ray™, DVD and On-Demand on May 15. The film is packed with light-hearted fun, pulse-pounding action and a powerful message.
To further celebrate these collaborations, Bulleit worked with local artists across the country to transform their Impressions into unique works of art. Contributors to-date include: Christina Mrozik and Zach Johnsen whose artwork debuted at Soul'd Out in Portland and Ryan Duggan and CHema Skandal! whose artwork was launched at One of Kind Festival in Chicago. Taylor McKimens and Tara McPherson's artwork is currently displayed throughout New York and will be showcased at Frieze New York.
Thanks to Universal Pictures Home Entertainment you can win a Den of Thieves prize pack. It is O'Shea Jackson and Ice Cube prize pack. You can win the following items. All you need to do is tell us out of the five films below which is the one you can't wait to see. Let me know on my Twitter account here. This prize pack giveaway is to celebrate the release of Den of Thieves out now on Blu-ray and digital.
The inevitable clash between an unbeatable crew addicted to heists and an elite unit of Los Angeles detectives comes to a head in Den of Thieves, arriving on Digital on April 10, 2018 and on Blu-ray™, DVD and On Demand on April 24, 2018 from STXfilms and Universal Pictures Home Entertainment. Directed by Christian Gudegast, the film blurs the lines between good and evil by examining the rivalry between a police unit, led by Gerard Butler), and a crew of career criminals featuring Curtis "50 Cent" Jackson, Pablo Schreiber, O'Shea Jackson Jr. and Evan Jones.
Den of Thieves is a gritty Los Angeles crime saga which follows the intersecting and often personally connected lives of an elite unit of the LA County Sheriff's Department and the state's most successful bank robbery crew as they plan a seemingly impossible heist on the Federal Reserve Bank of Downtown Los Angeles.


Product prices and availability are accurate as of 2019-04-05 19:54:39 EDT and are subject to change. Any price and availability information displayed on http://www.amazon.com/ at the time of purchase will apply to the purchase of this product.
Gates water pumps are recognized in the industry for their durability and superior quality. Every Gates water pump is constructed with premium alloy materials and a 100% functional test of seals and bearings along with a 100% leakage test for both the casting and assembled water pump.


Our people are our greatest asset – a team of people who are handpicked for their expertise, knowledge and affinity with the global dairy industry.
Together we share a commitment to continually push the boundaries of innovation and design, every day testing, reviewing and improving to ensure our systems and technologies are the best they can be.
We are innovative, adaptive and agile, focused on finding a way to solve the problems and challenges faced by farmers around the world.
Our goal is to continually exceed expectations, creating inventive but very real dairy solutions.
This close alignment enables oversight of every aspect of each product resulting in the unparalleled product quality and consistency we are known for.
Our New Zealand located global headquarters is the central manufacturing and design facility for core technologies, including three engineering divisions; an additional worldwide sales, engineering and distribution office in Janesville, WI and as well, additional engineering & manufacturing located in Holmen, WI.
Our experienced and dedicated US staff play an important strategic role in developing our global presence.


Alison offers free, online courses tailored to students learning English to increase their business potential. Start from the ground up with courses in grammar fundamentals, vocabulary, and more. Students can advance their learning with more in–depth courses on business terms and more.
In Alison's free, online business and travel English language course, you will be introduced to conversational vocabulary and dialogues to help you learn English language skills in the areas of business and travel.
In Alison's English language courses, you will listen to dialogues about job interviews and how to ask and answer interview questions in English.
Through these business English lessons, you will take the first step toward forming crucial business relationships.


text:
Closure Confusion
As is expected from purveyors of incompetence - TransLink, they have dropped another clanger over the information provided for this weekend's mass line closure through Brisbane's CBD. Taking in mind that the Ferny Grove Line is still running from Ferny Grove to Bowen Hills, the reasons for the closure has been given as - Turnout maintenance (Fortitude Valley), Embankment stabilisation (Newmarket), Removal of ground frames (Newmarket), Track reconditioning (Windsor to Wilston), Re-railing works (Bowen Hills to Windsor & Newmarket to Alderley), Overhead power line maintenance, General track maintenance. Given that many of these locations are on the apparently still open Ferny Grove Line, it seems that TransLink have done a half-arsed copy and paste job from a previous track closure. This has caused confusion, with many Ferny Grove Line commuters now unsure as to whether the Ferny Grove Line is actually running or not?
Showing how clueless Brisbane's journalists are, the Brisbane Times have copied and pasted this incorrect information into a news article without giving any thought to whether it was correct or not. Not much hope for investigative journalism is there!
This is not the first time TransLink have provide incorrect information regarding line closures. Last year, BrizCommuter was turfed off a late night northbound train at Park Road, when the TransLink's line closure information stated that trains were terminating at Roma Street. In fact, the train terminated at South Brisbane.
Ferny Grove Line commuters aren't the only ones confused by this weekends shenanigans. Long suffering Doomben Line commuters will have to alight at Wooloowin, then catch the next train (which could be quite a wait given the infrequency of trains in Brisbane) to travel one more stop to Albion, before then having to catch a rail replacement bus into the city or beyond. Anyone not prepared for this (which will be the majority) will end up arriving at their destination very very late!
Labels: Bus, CBD, Closures, Doomben, Ferny Grove, Incompetent, Incorrect, Line, Maintenance, Rail, Replacement, Track, Wrong
Spinning Out of Control
In BrizCommuter's previous blog post about the impending Gold Coast 2018 Commonwealth Games Rail Fail, it was alluded to that Queensland politicians are wrapped up in in their own spin. In this blog post BrizCommuter dissects some recent press releases, to show how out of touch politicians are from the land of reality. By the way, this blog post was almost going to be called "how big is the Transport Minister's weenie", but BrizCommuter thought better. If you want to know why - keep reading!
(Note: BrizCommuter apologises for the excessive use of quotation marks within quotation marks).
From the Brisbanetimes "Mr Beattie said the transport plan would provide terrific gains for the Commonwealth Games, "with a little pain". There is no gain, without some pain," he said." There would have been a lot less pain if the Beattie government had quad tracked Salisbury to Kuraby instead of only triplicating it!
From the Brisbanetimes "Transport and Main Roads Minister Mark Bailey said the government had kept its commitment not to close any rail lines during the games, but admitted commuters who used several stations along the Beenleigh line would be forced to use free buses instead." So "several" means 16? The dictionary definition of Several is "more than two but not many".
From the ABC news "Queensland Premier Annastacia Palaszczuk told Parliament last year "categorically" there would be no line closures, but Mr Bailey insisted the Beenleigh line would have a "continuous service" with the replacement buses." What is a "continuous service"? A conveyer belt? Be honest, you've closed 16 stations on one line, and replaced them with buses!
Transport Minister Mark Bailey discussing the Beenleigh Line rail replacement buses on ABC news "It will be quite convenient. In some cases it will actually be faster, in some cases it will be similar, and in some cases it will be a weeny-bit slower". A weenie bit slower? So having to walk to the bus stop (in some cases hundreds of metres from the station), wait for a bus, travel on the bus, walk from the bus to the platform, wait for the train, and then travel on the train, is only a weenie bit slower than just getting on a train? Maybe Mr Bailey has a very large weenie?
From the Courier Mail "Mr Bailey said the cuts were happening during the "usually quiet" Easter school holiday period." There are cuts for the whole week after the games and Easter holidays have finished. This will result in severe train overcrowding, and very annoyed commuters!
From the Brisbanetimes, quoting QR CEO Nick Easy "We've always said there would need to be some adjustments to other parts of the rail network as we increase services on the Gold Coast line to meet this unprecedented demand." So "some", means up to a 50% reduction in peak services, removal of some peak express services, up to 50% reduction of off-peak services, awful hourly weekend frequencies, closure of 16 Beenleigh Line stations, and 100% reduction of Ferny Grove to South Bank/Brisbane journeys. Right?
QR CEO Nick Easy "Other transport providers around the world have also had to alter their regular timetables to meet the demands of major events of this scale." The London Olympics, and Glasgow Commonwealth Games only saw service increases, not decreases. In Delhi, a few new metro lines opened for the Commonwealth Games. In SE Queensland, it's just a disaster!
When politicians are this delusional, there is little hope!
Labels: 2018, CEO, Commonwealth Games, Cuts, Fail, GC2018, Gold Coast, Political, politician, Press, QR, Queensland Rail, Rail, Spin
Fail McFailFace - QR's Comm Games Train Service
The Beenleigh Line has been axed!
As predicted by BrizCommuter back in November 2016 after the onset of #RailFail, Queensland Rail's (QR) services for the Gold Coast 2018 Commonwealth Games (GC2018) timetable will be a international embarrassment to Australia. Whilst the actual timetables have inconveniently yet to be been published, the service frequencies have been announced by TransLink. In order to increase frequencies on the Gold Coast Line to every 10 minutes or better, and allow for a rather pointless hourly overnight (24 hour) service, services have been heavily cut on all other train lines in Brisbane and SE Queensland. The frequencies on a line by line basis are available here:
So what is wrong with QR's GC2018 train service?
Overall 6% reduction in train services at a time when more are required.
The Beenleigh Line has been axed, due to the lack of infrastructure to allow faster Gold Coast Line services to overtake slower Beenleigh Line services in both directions. The Gold Coast services will serve 4 additional Beenleigh Line stations, thus slowing down the Gold Coast Line service in the process. Beenleigh Line passengers from the 16 non-serviced stations will have to use (thankfully free to use) rail replacement buses, massively slowing down commutes.
Peak services have been axed on all train lines other than the Gold Coast Line. It is expected that the peak service will be similar to that for the 2017/18 Summer Holiday Interim Timetable (SHIT) which was inadequate, resulted in overcrowded services, removed express services, and cut some direct journey opportunities by up to 75%. TransLink are still somehow charging peak fares!
Weekday 15 minute off-peak services have been axed on all lines other than the Gold Coast Line, with a 30 minute weekday off-peak services on all lines. That's a worse frequency than in Sydney, Melbourne, Perth, and Auckland!
Weekend off-peak services have been reduced by 50%. Services have been reduced from every 30 minutes to only every 60 minutes on all lines other than the Gold Coast and Airport Lines. This is worse than all other heavy rail systems in Australia and New Zealand! These hourly weekend services will be a nightmare for Brisbanites who need to get work, such as hospital and emergency service workers, retail and hospitality staff.
Long suffering Sunshine Coast Line commuters will see off-peak services reduced to an abysmal two hourly service.
The poor frequency of connecting services (especially on weekends) makes travelling by train to GC2018 extremely unattractive for both staff and spectators, forcing these commuters onto the already busy M1 Pacific Highway.
Bus to train connections around Brisbane and SE Queensland will be broken due to the large number of train services being cut and changes to clockface times.
The actual timetables (rather than service frequencies) are still unavailable two months out from GC2018.
The commuter pain will not just be during GC2018, but cover a 22 day period including Easter and a whole week after the games. Expect severe train overcrowding in the week beginning 16th April.
Who is to blame for this disastrous train service?
The Newman LNP government for stopping driver recruitment and demoting driver trainers, at a time when more train services were being added, or due to be added.
The Newman LNP government ordered NGR trains that were not compliant with disability access laws and regulations.
The Palaszczuk ALP government for mediocrity in fast tracking driver recruitment by giving in to Union demands and pressure.
The Palaszczuk ALP government for being slow at resolving NGR train issues, that were known about in 2015.
The Beattie/Bligh era ALP government for triplicating instead of quadding parts of the Gold Coast/Beenleigh Line corridor, thus preventing Gold Coast Line services from overtaking Beenleigh Line services in both directions (hence the axing of the Beenleigh Line).
The Beattie/Bligh era ALP government for dropping an add on order for the previous generation of trains, resulting in a lack of trains since 2014.
Rail Transport and Bus Union - for trying to block and delay external driver recruitment.
QR for chronically failed management. (Note: BrizCommuter has more faith in the new management, but the scale of past failure for them to fix is of epic proportions).
Department of Transport and Main Roads - this government department has overseen NGR Fail, Moreton Bay Rail Link Signalling Fail, QR's Rail Fail, and GC2018 Timetable Fail.
CityTrain Response Unit - for well, not doing much at all.
Queensland's media for being being hopeless at investigative journalism and reporting pressing transport issues. Authorities in cities such as London for example, could not get away with the mediocrity that QR and the various Queensland Governments have got away with in the last decade.
Whilst QR have tried to do the best with limited drivers and trains, the services frequencies for GC2018 show how bad #RailFail still is. Commuters on all train lines other than the Gold Coast Line will suffer from an inadequate "third world" train service between Friday 30th March and 20th April. Using trains to get to the games will be very unattractive due to poor service frequencies on connecting train lines, forcing more cars (including BrizCommuter's) onto the already congested M1 Pacific Highway. The only saviour to congestion chaos will be SE Queenslander's trying to avoid the dreaded M1 like the plague.
The only positive that BrizCommuter can see, is if Queensland and Australia is embarrassed so much on the world stage, that it sparks changes to governance and investment for SE Queensland's failing public transport system. However, given that QLD politicians are so lost in their own spin, BrizCommuter isn't convinced there will be much change to the current status quo of mediocrity.
Labels: #RailFail, 2018, Axed, Beenleigh, Commonwealth, Disaster, Embarrassment, Fail, Games, Gold Coast, Hourly, Line, QR, Queensland Rail, Rail, Services, Train

redpajama_set_name: RedPajamaCommonCrawl


text:
Q: adding new syntax formatting to existing filetype So I have some custom vi coloring in a syntax file.
This works perfectly (giving a blue background to lines 20-30)
syntax region mytest start="\%20l" end="\%30l"

hi mytest ctermbg=Blue

This does not work at all (attempting to give a blue background to a range that starts with x and ends with y):
syntax region mytest2 start="x" end="y" fold transparent

hi mytest2 ctermbg=Blue

I'm trying to figure out how to give formatting between a start/end point.  Any help is appreciated.

A: I misunderstood the function of "transparent".  Removing transparent fixes the issue.

redpajama_set_name: RedPajamaStackExchange


text:
Home Bollywood Film industry demands 'special court' to curb piracy
Film industry demands 'special court' to curb piracy
New Delhi–Members of the Film Federation of India (FFI) here on Tuesday urged the government to pay attention to issues ailing the film industry and sought creation of "special courts" for the industry.
"we want the government to tackle the problem of piracy by creating special courts where our pleas can be considered seriously," FII Secretary-General Supran Sen said.
Many a times when we approached police with complaints about issues like piracy, they ignored us, he said and added a special court would certainly help resolve the issue.
Hailing the government decision of reduce the goods and services tax (GST) on cinema tickets, the FFI said issues like piracy and restoration of single screens should also be looked into.
"The government decision of including an anti-camcording provision in the Cinematography Act to control piracy will need serious implementation," FFI's Vice-President Ramesh Tekwani told IANS.
Announcement is fine but people should be stopped from recording films by asking them to deposit their phones before entering theatres, he said. "One single phone can shoot the entire film. People also know different ways to record the film," Tekwani said.
FFI members Sakshi Mehra and G.D. Mehta said "restricted access due to lack of shows and unrestricted ticket prices" were increasing the piracy of films in India.
According to FFI President Firdausul Hasan, piracy of Indian films has crossed borders. "In Bangladesh, people watch our films, but they are not getting released there. This is piracy. Being a part of the FFI, we can just raise our voice, it's the government who has to do the real job," he said.
"To maintain better relations, we are creating an Indo-Bangla federation. Without the the government's help, we won't be able to do it. The government's co-operation is very important," Hasan said.
The FFI asked the government to stop "looking at the Indian film industry as sin industry".
On the issue of animal welfare board, Hasan said, "Animal welfare board is in Haryana. It takes a lot of time to take approval from them. It would be helpful if at least two members from the film fraternity are incorporated in the board." (IANS)
Previous articleFaridabad students win Microsoft's Asia contest, set eyes on US finals
Next articleOne hour a day can change your life: Robin Sharma


On March 18, Pope Benedict XVI appointed Redemptorist Father Ralph Heskett of the London Province, as the Bishop of Gibraltar. He will succeed Bishop Charles Caruana, KHS., C.St.J., who has surpassed retirement age and was awaiting a successor. Episcopal ordination is expected sometime in June.
Father Heskett was born on March 3, 1953 in Sunderland, England. He grew up in the Redemptorist parish of St. Benet's. He professed first vows as a Redemptorist on August 28, 1971 and made perpetual profession on August 30, 1974. He was ordained on July 10, 1976.
He has worked in mission and retreat ministries, and has served as vocations director and novice master for the London Province.
Gibraltar is situated at the southern end of the Iberian peninsula at the mouth of the Mediterranean Sea. Besides its interesting geological formation — it is commonly referred to as the "Rock" — Gibraltar also has an interesting political, religious and ecclesiastical history. Today, governmentally, Gibraltar is a British oversees territory. Before the 18th century, however, it was part of Spain and part of the diocese of Cádiz, Spain.
Bishop-elect Heskett becomes the third member of the London province to be raised to the episcopacy after Bishop Robert Aston Coffin, bishop of Southwick, England (1882-1885) and Bishop Hugh McDonald, bishop of Aberdeen, Scotland (1890-1898).


text:
import { UserId } from '../users/entities.users'
import { BBox, Feature, Geometry } from 'geojson'
import { MageEvent, MageEventAttrs, MageEventId } from '../events/entities.events'
import { PageOf, PagingParameters, PendingEntityId } from '../entities.global'
import { Form, FormField, FormFieldType, FormId } from '../events/entities.events.forms'
import * as fields from './entities.observations.fields'
import { JsonPrimitive } from '../entities.json_types'

export type ObservationId = string

export interface ObservationAttrs extends Feature<Geometry, ObservationFeatureProperties> {
  id: ObservationId
  eventId: MageEventId
  userId?: UserId
  // TODO: should be a strongly typed id-type
  deviceId?: string
  createdAt: Date
  lastModified: Date
  attachments: readonly Attachment[]
  important?: Readonly<ObservationImportantFlag> | undefined
  /**
   * TODO: scalability - potential problem if thousands of users favorite;
   * this should not be returned to the client
   */
  favoriteUserIds: readonly UserId[]
  /**
   * * TODO: scalability - likely not a problem in practice most of the time
   * * TODO: we do not actually have a reason to maintain an array of states -
   *   state should just be a single value object, and should not need
   *   a unique id
   */
  states: readonly ObservationState[]
}

export interface ObservationFeatureProperties {
  /**
   * This timestamp is a user-supplied timestamp that indicates the actual time
   * the observation occurred.
   */
  timestamp: Date
  forms: FormEntry[]
}

export interface ObservationImportantFlag {
  userId?: UserId
  timestamp?: Date
  description?: string
}

export interface ObservationState {
  id: string | PendingEntityId
  name: 'active' | 'archived'
  userId?: UserId | undefined
}

export type FormEntryId = string

/**
 * TODO: create strong types and union for form field values, basically json
 * primitives along with attachment array and geojson geometry object
 */
export interface FormEntry {
  id: FormEntryId
  formId: FormId
  [formFieldName: string]: FormFieldEntry
}

export type FormFieldEntryItem = Exclude<JsonPrimitive, null> | Geometry | Date
export type FormFieldEntry = FormFieldEntryItem | FormFieldEntryItem[] | null

export type AttachmentId = string
/**
 * TODO: Currently the web app uses the `name` and `contentType` keys in the
 * attachment object to correlate pending file uploads to newly saved
 * attachments.  While this works most of the time, especially for the web
 * when uplaods are nearly immediate, maybe something like a `pendingUploadId`
 * key would be more reliable for correlating a saved attachment record to
 * the file that the client intends to upload for that attachment.
 */
export interface Attachment {
  /**
   * Attachment IDs are globally unique, not unique only in the context of an
   * observation or form entry.
   */
  id: AttachmentId
  observationFormId: FormEntryId
  fieldName: string
  /**
   * TODO: Nothing seems to use this property.  Should we remove it, or
   * actually use it to inform browser caching?
   */
  lastModified?: Date
  /**
   * The content type is an IANA standard media type string, e.g., `image/jpeg`.
   */
  contentType?: string
  size?: number
  name?: string
  width?: number
  height?: number
  /**
   * The attachment's content locator is an abstract term that mostly exists
   * to reconcile with the legacy design of storing the relative file system
   * path of an attachment's file on the attachment document itself.  However,
   * as MAGE transitions to cloud-native infrastructure, one can more easily
   * envision swapping some sort of cloud-based BLOB storage service for the
   * legacy local file system storage.  Renaming the old `relativePath`
   * property to `contentLocator` is an attempt to allow for saving a lookup key
   * that does not necessarily imply an underlying file system as the storage
   * layer.  Implementations of the abstract {@link AttachmentStore} interface
   * would assign their own lookup key to this property, although the intention
   * of that interface's design is to be completely opaque with respect to how
   * an implementation stores and indexes attachment content.  An attachment
   * store implementation may not use `contentLocator` at all.
   */
  contentLocator?: string
  oriented: boolean
  thumbnails: Thumbnail[]
}

export interface Thumbnail {
  minDimension: number
  /**
   * See {@link Attachment.contentLocator} for an explanation.
   */
  contentLocator?: string
  contentType?: string
  size?: number
  name?: string
  width?: number
  height?: number
}

export function copyObservationAttrs(from: ObservationAttrs): ObservationAttrs {
  return {
    id: from.id,
    eventId: from.eventId,
    userId: from.userId,
    deviceId: from.deviceId,
    createdAt: new Date(from.createdAt.getTime()),
    lastModified: new Date(from.lastModified.getTime()),
    attachments: from.attachments.map(copyAttachmentAttrs),
    important: from.important ? copyImportantFlagAttrs(from.important) : undefined,
    favoriteUserIds: Object.freeze([ ...from.favoriteUserIds ]),
    states: Object.freeze(from.states.map(copyObservationStateAttrs)),
    type: 'Feature',
    // meh, these shallow copies are probably fine ... right?
    geometry: Object.freeze({ ...from.geometry }),
    properties: { ...from.properties, forms: from.properties.forms.map(x => ({ ...x })), timestamp: new Date(from.properties.timestamp) }
  }
}

export function copyAttachmentAttrs(from: Attachment): Attachment {
  return {
    id: from.id,
    fieldName: from.fieldName,
    observationFormId: from.observationFormId,
    lastModified: from.lastModified ? new Date(from.lastModified) : undefined,
    contentType: from.contentType,
    name: from.name,
    size: from.size,
    width: from.width,
    height: from.height,
    oriented: from.oriented,
    thumbnails: from.thumbnails.map(copyThumbnailAttrs),
    contentLocator: from.contentLocator
  }
}

export function copyThumbnailAttrs(from: Thumbnail): Thumbnail {
  return {
    minDimension: from.minDimension,
    contentLocator: from.contentLocator,
    contentType: from.contentType,
    name: from.name,
    size: from.size,
    width: from.width,
    height: from.height
  }
}

export function copyObservationStateAttrs(from: ObservationState): ObservationState {
  return {
    id: from.id,
    name: from.name,
    userId: from.userId
  }
}

export function copyImportantFlagAttrs(from: ObservationImportantFlag): ObservationImportantFlag {
  return {
    userId: from.userId,
    description: from.description,
    timestamp: from.timestamp ? new Date(from.timestamp.getTime()) : undefined
  }
}

/**
 * The intention of this class is to provide a mutation model for observation
 * updates.  While `ObservationAttrs` is more just raw the keys and values of
 * observations, this class retains some extra context for validation so
 * the client can make changes to the observation data while receiving
 * validation feedback for every operation performed on the observation.
 * TODO: add domain event tracking to this class
 */
export class Observation implements Readonly<ObservationAttrs> {

  /**
   * Validate the given observation attributes against the given MAGE event's
   * constraints and forms, and create the corresponding `Observation` instance
   * with the validation result.
   * @param attrs
   * @param mageEvent
   */
  static evaluate(attrs: ObservationAttrs, mageEvent: MageEvent): Observation {
    return createObservation(attrs, mageEvent)
  }

  /**
   * TODO: This does not currently do anything besides assigning the
   * `lastModified` timestamp on the updated observation and calling
   * {@link Observation.evaluate()} with the given update attributes.
   * Eventually this should perform the logic to find the differences and
   * produce the domain events resulting from updating the target observation
   * to the update attributes.
   *
   * Return an {@link ObservationUpdateError} if the event IDs on the target
   * and udpate do not match.
   *
   * @param target
   * @param update
   * @returns
   */
  static assignTo(target: Observation, update: ObservationAttrs): Observation | ObservationUpdateError {
    if (update.eventId !== target.eventId) {
      return ObservationUpdateError.eventIdMismatch(target.eventId, update.eventId)
    }
    update = {
      ...update,
      createdAt: new Date(target.createdAt),
      lastModified: new Date()
    }
    const updateAttachments = update.attachments.reduce((updateAttachments, att) => {
      return updateAttachments.set(att.id, att)
    }, new Map<AttachmentId, Attachment>())
    const removedAttachments = target.attachments.filter(x =>!updateAttachments.has(x.id))
    // TODO: whatever other mods generate events that matter
    const updateEvents = removedAttachments.length ? [ AttachmentsRemovedDomainEvent(target, removedAttachments) ] : [] as PendingObservationDomainEvent[]
    const pendingEvents = mergePendingDomainEvents(target, updateEvents)
    return createObservation(update, target.mageEvent, pendingEvents)
  }

  #validation: ObservationValidationResult
  #formEntriesById: Map<FormEntryId, FormEntry>
  #attachmentsById: Map<AttachmentId, Attachment>

  readonly id: string
  readonly eventId: number
  readonly mageEvent: MageEvent
  /**
   * If an observation has no user ID, the user that created the observation
   * was removed from the system.
   */
  readonly userId?: UserId | undefined
  /**
   * If an observation has no device ID, the device that submitted the
   * observation was removed from the system.
   */
  readonly deviceId?: string | undefined
  readonly createdAt: Date
  readonly lastModified: Date
  readonly important?: Readonly<ObservationImportantFlag> | undefined
  readonly states: readonly ObservationState[]
  readonly favoriteUserIds: readonly UserId[]
  readonly type = 'Feature'
  readonly bbox?: BBox | undefined
  readonly geometry: Readonly<Geometry>
  readonly properties: Readonly<ObservationFeatureProperties>
  readonly attachments: readonly Attachment[]
  readonly pendingEvents: readonly PendingObservationDomainEvent[]

  constructor(...args: unknown[]) {
    if (args[0] !== ObservationConstructionToken) {
      throw new Error('use a factory function to create an observation instance')
    }
    const attrs = args[1] as ObservationAttrs
    this.mageEvent = args[2] as MageEvent
    this.id = attrs.id
    this.eventId = attrs.eventId
    this.userId = attrs.userId
    this.deviceId = attrs.deviceId
    this.createdAt = new Date(attrs.createdAt)
    this.lastModified = new Date(attrs.lastModified)
    this.important = attrs.important ? Object.freeze({ ...attrs.important }) : undefined
    const states = attrs.states.length ? attrs.states.map(copyObservationStateAttrs) : [
      {
        id: PendingEntityId,
        name: 'active',
        userId: attrs.userId
      }
    ] as ObservationState[]
    this.states = Object.freeze(states)
    this.favoriteUserIds = Object.freeze(attrs.favoriteUserIds.slice())
    this.type = 'Feature'
    this.bbox = attrs.bbox
    this.geometry = attrs.geometry
    this.attachments = Object.freeze([ ...attrs.attachments ])
    this.properties = { ...attrs.properties }
    this.#formEntriesById = new Map(this.properties.forms.map(x => [ x.id, x ]))
    this.#attachmentsById = new Map(this.attachments.map(x => [ x.id, x ]))
    this.#validation = args[3] as ObservationValidationResult
    this.pendingEvents = (args[4] || []) as PendingObservationDomainEvent[]
  }

  get validation(): ObservationValidationResult {
    return this.#validation
  }

  /**
   * This is a convenience accessor for {@link ObservationFeatureProperties.timestamp}.
   */
  get timestamp(): Date {
    return this.properties.timestamp
  }

  /**
   * This is a convenience accessor for {@link ObservationFeatureProperties.forms}.
   */
  get formEntries(): FormEntry[] {
    return Array.from(this.#formEntriesById.values())
  }

  formEntryForId(id: FormEntryId): FormEntry | null {
    return this.#formEntriesById.get(id) || null
  }

  attachmentFor(id: AttachmentId): Attachment | null {
    return this.#attachmentsById.get(id) || null
  }

  attachmentsForField(fieldName: string, formEntryId: FormEntryId): Attachment[] {
    return attachmentsForField(fieldName, formEntryId, this)
  }
}

export enum ObservationDomainEventType {
  AttachmentsRemoved = 'Observation.AttachmentsRemoved',
}

/**
 * A pending domain event waits for dispatch in the {@link Observation.pendingEvents | pending events}
 * on the observation instance that generated the event.  To avoid a circular
 * reference, pending events do not have a member for the observation instance
 * that generated them.
 */
export type PendingObservationDomainEvent = {
  readonly type: ObservationDomainEventType
} & (
  | {
    type: ObservationDomainEventType.AttachmentsRemoved
    readonly removedAttachments: readonly Readonly<Attachment>[]
  }
)

// export type ObservationDomainEvent = PendingObservationDomainEvent & {

//   readonly observation: Observation
// }

/**
 * This type simply adds the subject observation instance to the {@link PendingObservationDomainEvent}
 * so listeners of the event will have the observation that generated the
 * event when received.
 */
export type ObservationEmitted<Pending extends PendingObservationDomainEvent> = Pending & {
  /**
   * For now, this value is the snapshot of the observation just after the save
   * operation that initiated domain event dispatch.  Hence, this observation
   * will have no pending events and reflects the state of the observation
   * after the mutations that created the domain events.  That understanding
   * could change if more domain event requirements present themselves, but
   * this is sufficient for the, at this time, singular domain event that
   * supports attachment removal.
   */
  readonly observation: Observation
}

export type AttachmentsRemovedDomainEvent = Extract<PendingObservationDomainEvent, { type: ObservationDomainEventType.AttachmentsRemoved }>

export interface ObservationValidationResult {
  readonly hasErrors: boolean
  readonly coreAttrsErrors: { readonly [attr in ObservationValidationCoreAttrKey]?: string }
  readonly formCountErrors: readonly [ FormId, FormCountError ][]
  readonly formEntryErrors: readonly [ number, FormEntryValidationError ][]
  /**
   * This list contains attachment error map entries where the key is the
   * position of the attachment in the `attachments` array on the observation,
   * and the value is the `AttachmentValidationError`.
   */
  readonly attachmentErrors: readonly [ number, AttachmentValidationError ][]
  readonly totalFormCountError: TotalFormCountError | null
}

export function validateObservation(observationAttrs: ObservationAttrs, mageEvent: MageEvent): ObservationValidationResult {
  const validation =
    validateObservationFormEntries(
      validateObservationAttachments(
        validateObservationCoreAttrs(
          new ObservationValidationContext(observationAttrs, mageEvent))))
  return validation.result()
}

export type ObservationValidationCoreAttrKey = 'eventId' | 'type' | 'geometry' | 'timestamp' | 'forms'

export const MinFormsConstraint = Symbol('ObservationValidation.MinForms')
export const MaxFormsConstraint = Symbol('ObservationValidation.MaxForms')

export class TotalFormCountError {

  static tooFewFormEntries(mageEvent: MageEventAttrs): TotalFormCountError {
    return new TotalFormCountError(MinFormsConstraint, Number(mageEvent.minObservationForms))
  }

  static tooManyFormEntries(mageEvent: MageEventAttrs): TotalFormCountError {
    return new TotalFormCountError(MaxFormsConstraint, Number(mageEvent.maxObservationForms))
  }

  private constructor(
    readonly constraint: typeof MinFormsConstraint | typeof MaxFormsConstraint,
    readonly constraintCount: number) { }

  message(): string {
    if (this.constraint === MinFormsConstraint) {
      return `The event requires at least ${this.constraintCount} form ${this.constraintCount > 1 ? 'entries' : 'entry'}.`
    }
    return `The event allows at most ${this.constraintCount} form ${this.constraintCount > 1 ? 'entries' : 'entry'}.`
  }
}
export class FormCountError {

  static tooFewEntriesForForm(form: Form): FormCountError {
    return new FormCountError(MinFormsConstraint, Number(form.min), form.id, form.name)
  }

  static tooManyEntriesForForm(form: Form): FormCountError {
    return new FormCountError(MaxFormsConstraint, Number(form.max), form.id, form.name)
  }

  private constructor(
    readonly constraint: typeof MinFormsConstraint | typeof MaxFormsConstraint,
    readonly constraintCount: number,
    readonly formId: FormId,
    readonly formName: string
    ) { }

  message(): string {
    if (this.constraint === MinFormsConstraint) {
      return `The event requires at least ${this.constraintCount} ${this.formName} form ${this.constraintCount > 1 ? 'entries' : 'entry'}.`
    }
    return `The event allows at most ${this.constraintCount} ${this.formName} form ${this.constraintCount > 1 ? 'entries' : 'entry'}.`
  }
}

export class FormEntryValidationError {

  #entryLevelErrors: Set<FormEntryValidationErrorReason> = new Set()
  #fieldErrors: Map<string, FormFieldValidationError> = new Map()

  /**
   * If there is no form reference error, set the form name for convenience
   * when building the validation error message text.
   */
  formName: string | null = null

  constructor(readonly formEntryId: FormEntryId, readonly formEntryPosition: number) {}

  addEntryLevelError(x: FormEntryValidationErrorReason): this {
    this.#entryLevelErrors.add(x)
    return this
  }

  addFieldError(x: FormFieldValidationError): this {
    this.#fieldErrors.set(x.fieldName, x)
    return this
  }

  get entryLevelErrors(): Set<FormEntryValidationErrorReason> {
    return new Set(this.#entryLevelErrors)
  }

  /**
   * This is a map of field names to form field validation errors.
   */
  get fieldErrors(): Map<string, FormFieldValidationError> {
    return new Map(this.#fieldErrors)
  }
}

export enum FormEntryValidationErrorReason {
  FormRef = 'FormEntryValidationErrorReason.FormRef',
  DuplicateId = 'FormEntryValidationErrorReason.DuplicateId'
}

export type FormFieldValidationErrorAttrs = {
  [Prop in keyof Omit<FormFieldValidationError, 'error'>]: FormFieldValidationError[Prop]
}

export enum FieldConstraintKey {
  Value = 'value',
  Min = 'min',
  Max = 'max',
  Required = 'required'
}

export class FormFieldValidationError {

  readonly fieldName: string
  readonly constraint: FieldConstraintKey
  readonly message: string

  constructor(attrs: FormFieldValidationErrorAttrs) {
    this.fieldName = attrs.fieldName
    this.constraint = attrs.constraint
    this.message = attrs.message
  }

  /**
   * `error` is a readonly accessor alias for `validationRuleKey` for backward
   * compatibility.
   * TODO: verify this is still necessary (NECESSARY?!)
   */
  get error(): string {
    return this.constraint
  }
}

export enum AttachmentValidationErrorReason {
  FieldRef = 'AttachmentValidationErrorReason.FieldRef',
  FormEntryRef = 'AttachmentValidationErrorReason.FormEntryRef',
  DuplicateId = 'AttachmentValidationErrorReason.DuplicateId',
}

export class AttachmentValidationError {
  constructor(
    readonly reason: AttachmentValidationErrorReason,
    readonly message: string
  ) {}
}

export function validationResultMessage(result: ObservationValidationResult): string {
  const {
    coreAttrsErrors,
    totalFormCountError,
    formCountErrors,
    formEntryErrors,
    attachmentErrors,
  } = result
  const bulletPoint = '\u2022'
  const errList: string[] = []
  for (const message of Object.values(coreAttrsErrors)) {
    errList.push(`${bulletPoint} ${message}`)
  }
  if (totalFormCountError) {
    errList.push(`${bulletPoint} ${totalFormCountError.message()}`)
  }
  for (const [ formId, err ] of formCountErrors) {
    errList.push(`${bulletPoint} ${err.message()}`)
  }
  for (const [ formEntryId, formEntryErr ] of formEntryErrors) {
    errList.push(`${bulletPoint} Form entry ${formEntryErr.formEntryPosition + 1} (${formEntryErr.formName}) is invalid.`)
    for (const fieldErr of formEntryErr.fieldErrors.values()) {
      errList.push(`  ${bulletPoint} ${fieldErr.message}`)
    }
  }
  for (const [ pos, attachmentErr ] of attachmentErrors) {
    errList.push(`${bulletPoint} Attachment ${pos + 1} is invalid.  ${attachmentErr.message}`)
  }
  return errList.join('\n')
}

export class ObservationUpdateError extends Error {

  static eventIdMismatch(expected: MageEventId, actual: MageEventId | undefined): ObservationUpdateError {
    return new ObservationUpdateError(ObservationUpdateErrorReason.EventId,
      `The update event ID ${actual} does not match the target event ID ${expected}.`)
  }

  constructor(readonly reason: ObservationUpdateErrorReason, message: string) {
    super(message)
  }
}

export enum ObservationUpdateErrorReason {
  EventId = 'event_id'
}

export function formEntryForId(formEntryId: FormEntryId, observation: ObservationAttrs): FormEntry | null {
  return observation.properties.forms.find(x => x.id === formEntryId) || null
}

export function attachmentsForField(field: FormField | string, formEntry: FormEntry | FormEntryId, observationAttrs: ObservationAttrs): Attachment[] {
  const fieldName = typeof field === 'object' ? field.name : field
  const formEntryId = typeof formEntry === 'object' && 'id' in formEntry && 'formId' in formEntry ? formEntry.id : formEntry
  return observationAttrs.attachments.filter(x => x.fieldName === fieldName && x.observationFormId === formEntryId)
}

/**
 * Remove the form entry with the given ID and return the resulting
 * (potentially invalid) observation.
 *
 * TODO: add a `FormEntryNotFound` error similar to `removeAttachment()`
 */
export function removeFormEntry(observation: Observation, formEntryId: FormEntryId): Observation {
  const mod = copyObservationAttrs(observation)
  const targetPos = observation.formEntries.findIndex(x => x.id === formEntryId)
  mod.properties.forms.splice(targetPos, 1)
  mod.attachments = observation.attachments.filter(x => x.observationFormId !== formEntryId)
  return Observation.assignTo(observation, mod) as Observation
}

export type AttachmentCreateAttrs = Omit<Attachment, 'id' | 'observationFormId' | 'fieldName' | 'lastModified'>
export type AttachmentPatchAttrs = Partial<AttachmentCreateAttrs>
export type AttachmentContentPatchAttrs = Required<Pick<Attachment, 'contentLocator' | 'size'>>
export type ThumbnailContentPatchAttrs = Required<Pick<Thumbnail, 'contentLocator' | 'size'>> & Thumbnail

/**
 * Add the given attachment to the given observation.  Return a new observation
 * instance with the added attachment, or return an {@link AttachmentAddError}
 * if the given attachment does not reference a valid form entry and field.
 * Note that the returned observation may still have validation errors resulting
 * from the added attachment if the attachment violates the associated form
 * constraints, such as min/max or allowed attachment types.
 * @param observation
 * @param fieldName
 * @param formEntryId
 * @param attrs
 * @returns
 */
export function addAttachment(observation: Observation, attachmentId: AttachmentId, fieldName: string, formEntryId: FormEntryId, attrs: AttachmentCreateAttrs): Observation | AttachmentAddError {

  const attachment: Attachment = {
    ...attrs,
    id: attachmentId,
    fieldName,
    observationFormId: formEntryId,
    lastModified: new Date()
  }
  const invalid = validateAttachment(attachment, observation, observation.mageEvent)
  if (invalid) {
    return AttachmentAddError.invalidNewAttachment(invalid)
  }
  const mod = copyObservationAttrs(observation)
  mod.attachments = [ ...mod.attachments, attachment ]
  return Observation.assignTo(observation, mod) as Observation
}

/**
 * Update the attachment for the given ID with the given patch object.  Keys
 * that are present in the patch whose values are `undefined` will assign
 * `undefined` to the resulting updated attachment.  Keys not present in the
 * patch will have no affect on the resulting updated attachment.
 * @param observation
 * @param attachmentId
 * @param patch
 * @returns
 */
 export function patchAttachment(observation: Observation, attachmentId: AttachmentId, patch: AttachmentPatchAttrs): Observation | AttachmentNotFoundError {
  const targetPos = observation.attachments.findIndex(x => x.id === attachmentId)
  const target = observation.attachments[targetPos]
  if (!target) {
    return new AttachmentNotFoundError(attachmentId)
  }
  const patched = copyAttachmentAttrs(target)
  patched.contentType = patch.hasOwnProperty('contentType') ? patch.contentType : patched.contentType
  patched.height = patch.hasOwnProperty('height') ? patch.height : patched.height
  patched.width = patch.hasOwnProperty('width') ? patch.width : patched.width
  patched.name = patch.hasOwnProperty('name') ? patch.name : patched.name
  patched.oriented = patch.hasOwnProperty('oriented') ? !!patch.oriented : patched.oriented
  patched.size = patch.hasOwnProperty('size') ? patch.size : patched.size
  patched.contentLocator = patch.hasOwnProperty('contentLocator') ? patch.contentLocator : patched.contentLocator
  patched.thumbnails = patch.hasOwnProperty('thumbnails') ? patch.thumbnails?.map(copyThumbnailAttrs) as Thumbnail[] : patched.thumbnails
  patched.lastModified = new Date()
  const patchedObservation = copyObservationAttrs(observation)
  const before = patchedObservation.attachments.slice(0, targetPos)
  const after = patchedObservation.attachments.slice(targetPos + 1)
  const attachments = before.concat(patched, after)
  patchedObservation.attachments = Object.freeze(attachments)
  patchedObservation.lastModified = new Date(patched.lastModified)
  return Observation.assignTo(observation, patchedObservation) as Observation
}

export function removeAttachment(observation: Observation, attachmentId: AttachmentId): Observation | AttachmentNotFoundError {
  const targetPos = observation.attachments.findIndex(x => x.id === attachmentId)
  const target = observation.attachments[targetPos]
  if (!target) {
    return new AttachmentNotFoundError(attachmentId)
  }
  const afterAttrs = copyObservationAttrs(observation)
  const attachments = afterAttrs.attachments.slice()
  attachments.splice(targetPos, 1)
  afterAttrs.attachments = attachments
  return Observation.assignTo(observation, afterAttrs) as Observation
}

/**
 * Add the given thumbnail to the given attachment.  If the attachment already
 * has a thumbnail at the same minimum dimension as the given thumbnail,
 * replace the existing thumbnail with the given thumbnail at the same position
 * in the thumbnails array.
 * @param observation
 * @param attachmentId
 * @param thumbnail
 * @returns
 */
export function putAttachmentThumbnailForMinDimension(observation: Observation, attachmentId: AttachmentId, thumbnail: Thumbnail): Observation | AttachmentNotFoundError {
  const target = observation.attachmentFor(attachmentId)
  if (!target) {
    return new AttachmentNotFoundError(attachmentId)
  }
  const thumbnailsPatch = target.thumbnails.map(copyThumbnailAttrs)
  const targetThumbPos = thumbnailsPatch.findIndex(x => x.minDimension === thumbnail.minDimension)
  const putThumbAttrs = copyThumbnailAttrs(thumbnail)
  if (targetThumbPos >= 0) {
    thumbnailsPatch[targetThumbPos] = putThumbAttrs
  }
  else {
    thumbnailsPatch.push(putThumbAttrs)
  }
  return patchAttachment(observation, attachmentId, { thumbnails: thumbnailsPatch })
}

/**
 * Return the index of the thumbnail on the given attachment that best satisfies
 * the given target dimension.  That will be the thumbnail with the smallest
 * {@link Attachment.minDimension} greater than the target dimension.
 */
export function thumbnailIndexForTargetDimension(targetDimension: number, attachment: Attachment): number | undefined {
  if (attachment.thumbnails.length === 0) {
    return void(0)
  }
  return attachment.thumbnails.reduce<number | undefined>((best, candidate, index) => {
    if (candidate.minDimension >= targetDimension &&
      (best === undefined || candidate.minDimension < attachment.thumbnails[best].minDimension)) {
      return index
    }
    return best
  }, void(0))
}

export class AttachmentAddError extends Error {

  static invalidNewAttachment(invalidErr: AttachmentValidationError): AttachmentAddError {
    return new AttachmentAddError(invalidErr)
  }

  constructor(readonly validationErr: AttachmentValidationError) {
    super(`Invalid new attachment: ${validationErr.message}`)
  }
}

export class AttachmentNotFoundError extends Error {
  constructor(readonly attachmentId: AttachmentId) {
    super(`Attachment ID ${attachmentId} does not exist.`)
  }
}

/**
 * This repository provides persistence operations for `Observation` entities
 * within the scope of one MAGE event.
 */
export interface EventScopedObservationRepository {
  readonly eventScope: MageEventId
  allocateObservationId(): Promise<ObservationId>
  /**
   * Save the given observation.  This operation uses PUT semantics, so
   * essentially overwrites an existing observation with the given attributes.
   * Therefore, the risk of conflicting udpates exists, when two or more
   * clients fetch the same version of an observation, and each client makes
   * different changes to the observation, the client that saves its changes
   * last will win, overwriting the changes the other clients saved.
   * @param observation
   */
  save(observation: Observation): Promise<Observation | ObservationRepositoryError>
  findById(id: ObservationId): Promise<Observation | null>
  /**
   * Return the most recent observation in the event as determined by
   * the observation's `lastModified` timestamp.  Return null if there are no
   * observations in the event.
   * @returns an `Observation` object or `null`
   */
  findLatest(): Promise<ObservationAttrs | null>
  findLastModifiedAfter(timestamp: number, paging: PagingParameters): Promise<PageOf<ObservationAttrs>>
  /**
   * Update the specified attachment with the given attributes.  This
   * persistence function exists alongside the {@link save} method to prevent
   * concurrent updates from colliding and overwriting each other.  That
   * situation is common when storing attachment content because a single
   * client will intiate several parallel content uploads for multiple
   * attachments.  Each separate content upload could fetch the same version of
   * the observation from the database, store the content, then update that
   * version of the observation record.  If using the whole observation `save()`
   * method to apply the update just for a single attachment, each save
   * operation would overwrite the changes for the save operations that came
   * before.  Return the updated `Observation` entity instance on a successful
   * update.  Return an `AttachmentNotFoundError` if the given observation did
   * not have an attachment with the given attachment ID.  Return null if the
   * given observation does not exist in the database.
   *
   * Note that this is a shallow patch, so if the patch has the `thumbnails`
   * key, the method will persist the value of that key to the database, as
   * opposed to deeply patching any thumbnails present in the array.  This
   * method should essentially use {@link patchAttachment} to perform the
   * patch for the resulting attachment.
   *
   * TODO: Account for patch invalidating the attachment media type.
   */
  patchAttachment(observation: Observation, attachmentId: AttachmentId, contentInfo: AttachmentPatchAttrs): Promise<Observation | AttachmentNotFoundError | null>
  /**
   * Because attachments reference a form entry by its ID, an API to generate
   * form entry IDs is necessary.
   */
  nextFormEntryIds(count?: number): Promise<FormEntryId[]>
  nextAttachmentIds(count?: number): Promise<AttachmentId[]>
}

export class ObservationRepositoryError extends Error {

  constructor(readonly code: ObservationRepositoryErrorCode, message?: string) {
    super(message)
  }
}

export enum ObservationRepositoryErrorCode {
  InvalidObservationId = 'ObservationRepositoryError.InvalidObservationId',
  InvalidObservation = 'ObservationRepositoryError.InvalidObservation'
}

export interface ObservationRepositoryForEvent {
  (event: MageEventId): Promise<EventScopedObservationRepository>
}

export type StagedAttachmentContentId = unknown

export class StagedAttachmentContentRef {
  constructor(readonly id: StagedAttachmentContentId) {}
}

export class StagedAttachmentContent extends StagedAttachmentContentRef {
  constructor(
    id: StagedAttachmentContentId,
    readonly tempLocation: NodeJS.WritableStream
  ) {
    super(id)
  }
}

/**
 * TODO: Maybe instead of the `null | Observation | AttachmentStoreError`
 * pattern many of these method signatures use, a single `AttachmentStoreResult`
 * class with a `success` flag and `observation` and `error` properties would
 * be eaiser for clients to consume.
 */
export interface AttachmentStore {
  /**
   * Create a temporary staging area to hold attachment content pending
   * persistence of the associated attachment record to the observation in the
   * database.  After saving the attachment record to the parent observation,
   * the client must move the temporary content to the proper permanent
   * location for the  saved attachment using the {@link saveContent()} function.
   * This mechanism is useful in cases when a content stream exists, but the
   * provider of the content stream needs to examine the bytes while streaming
   * to the temporary location in order to determine the attachment meta-data.
   * This mechanism also facilitates periodic cleanup of orphaned temporary
   * content.
   */
  stagePendingContent(): Promise<StagedAttachmentContent>
  /**
   * Save the given content to the store for the specified attachment.  If the
   * `content` argument is an ID for {@link StagedAttachmentContent staged content},
   * the store will move the content at the temporary location to the permanent
   * location for the specified attachment.  If the store assigns a new
   * {@link Attachment.contentLocator | content locator} to the attachment after
   * a successful save, and/or changes the size of the attachment to the actual
   * number of bytes written, return a {@link AttachmentContentPatchAttrs patch}
   * to {@link EventScopedObservationRepository.patchAttachment update}
   * the attachment  new observation instance with the {@link patchAttachment | patched}
   * attachment.  Return `null` if the save succeeded and no change to the
   * attachment was necessary.  Return an {@link AttachmentStoreError} if the
   * save failed.
   */
  saveContent(content: NodeJS.ReadableStream | StagedAttachmentContentRef, attachmentId: AttachmentId, observation: Observation): Promise<null | AttachmentContentPatchAttrs | AttachmentStoreError>
  /**
   * Similar to {@link saveContent()}, but for thumbnails of attachments.
   * The store distinguishes thumbnails by their standard minimum dimension.
   * If the `contentLocator` or the `size` of the stored thumbnail content is
   * different than what is on the input thumbnail meta-data, return thumbnail
   * attributes suitable to pass to {@link putAttachmentThumbnailForMinDimension}
   * to update the observation with the new attachment thumbnail.
   */
  saveThumbnailContent(content: NodeJS.ReadableStream | StagedAttachmentContentId, minDimension: number, attachmentId: AttachmentId, observation: Observation): Promise<null | ThumbnailContentPatchAttrs | AttachmentStoreError>
  /**
   * Return a read stream of the content for the given attachment.  The client
   * can specify an optional zero-based range of bytes to read from the
   * content, which will return a read stream limited the specified range.
   * Note that the end index of the range is inclusive, as is the case with
   * Node's streams API, as opposed to array and string operations, for
   * which the end index is usually exclusive.  Return `null` if not content
   * exists for the given attachment.  Return an `AttachmentStoreError` if
   * an error occurred reading from the underlying storage.
   */
  readContent(attachmentId: AttachmentId, observation: Observation, range?: { start: number, end?: number }): Promise<NodeJS.ReadableStream | null | AttachmentStoreError>
  readThumbnailContent(minDimension: number, attachmentId: AttachmentId, observation: Observation): Promise<NodeJS.ReadableStream | null | AttachmentStoreError>
  /**
   * Delete the given attachment's content, including thumbnail content.  If
   * the attachment no longer exists on the given observation, return null upon
   * success.  If the attachment still exists on the observation, return
   * {@link AttachmentContentPatchAttrs patch} attributes that reflect the
   * missing content to {@link EventScopedObservationRepository.patchAttachment update}
   * the attachment.
   */
  deleteContent(attachment: Attachment, observation: Observation): Promise<null | AttachmentPatchAttrs | AttachmentStoreError>
}

export class AttachmentStoreError extends Error {

  static invalidAttachmentId(attachmentId: AttachmentId, observation: Observation): AttachmentStoreError {
    return new AttachmentStoreError(AttachmentStoreErrorCode.InvalidAttachmentId, `observation ${observation.id} has no attachment ${attachmentId}`)
  }

  static invalidThumbnailDimension(minDimension: number, attachmentId: AttachmentId, observation: Observation): AttachmentStoreError {
    return new AttachmentStoreError(AttachmentStoreErrorCode.InvalidThumbnailDimension, `attachment ${attachmentId} on observation ${observation.id} has no thumbnail with dimension ${minDimension}`)
  }

  constructor(readonly errorCode: AttachmentStoreErrorCode, message?: string) {
    super(message)
    this.name = errorCode
  }
}

export enum AttachmentStoreErrorCode {
  /**
   * The given attachment ID was not found in the given observation's
   * attachment list.
   */
  InvalidAttachmentId = 'AttachmentStoreError.InvalidAttachmentId',
  InvalidThumbnailDimension = 'AttachmentStoreError.InvalidThumbnailDimension',
  /**
   * The content for the given attachment ID was not found in the attachment
   * store.
   */
  ContentNotFound = 'AttachmentStoreError.ContentNotFound',
  /**
   * The underlying storage system, e.g. file system, raised an error during
   * some I/O operation.
   */
  StorageError = 'AttachmentStoreError.StorageError'
}

const ObservationConstructionToken = Symbol('ObservationConstructor')

function createObservation(attrs: ObservationAttrs, mageEvent: MageEvent, pendingEvents?: readonly PendingObservationDomainEvent[]): Observation {
  attrs = copyObservationAttrs(attrs)
  const validation = validateObservation(attrs, mageEvent)
  return new Observation(ObservationConstructionToken, attrs, mageEvent, validation, pendingEvents)
}

function validateObservationCoreAttrs(validation: ObservationValidationContext): ObservationValidationContext {
  const { observationAttrs, mageEvent } = validation
  if (observationAttrs.eventId !== mageEvent.id) {
    validation.addCoreAttrsError('eventId', `The observation event ID ${observationAttrs.eventId} does not match the asserted event ID ${mageEvent.id}.`)
  }
  if (observationAttrs.type !== 'Feature') {
    validation.addCoreAttrsError('type', `The observation GeoJSON type must be 'Feature'.`)
  }
  const invalidGeometry = fields.geometry.GeometryFieldValidation(
    { title: 'Geometry', id: 0, name: 'geometry', required: true, type: FormFieldType.Geometry },
    observationAttrs.geometry as any,
    { succeeded: () => null, failedBecauseTheEntry: reason => reason })
  if (invalidGeometry) {
    validation.addCoreAttrsError('geometry', `The observation geometry ${invalidGeometry}`)
  }
  if (!(observationAttrs.properties.timestamp instanceof Date)) {
    validation.addCoreAttrsError('timestamp', 'The observation requires a valid timestamp.')
  }
  return validation
}

function validateObservationFormEntries(validation: ObservationValidationContext): ObservationValidationContext {
  const { observationAttrs, mageEvent } = validation
  if (!Array.isArray(observationAttrs.properties.forms)) {
    return validation.addCoreAttrsError('forms', 'The observation requires an array of form entries.')
  }
  const formEntryIds = new Set<FormEntryId>()
  const formEntryCounts = observationAttrs.properties.forms.reduce((formEntryCounts, formEntry, formEntryPos) => {
    const formEntryError = new FormEntryValidationError(formEntry.id, formEntryPos)
    if (formEntryIds.has(formEntry.id)) {
      formEntryError.addEntryLevelError(FormEntryValidationErrorReason.DuplicateId)
    }
    formEntryIds.add(formEntry.id)
    const form = mageEvent.formFor(formEntry.formId)
    if (form) {
      if (!form.archived) {
        formEntryError.formName = form.name
        formEntryCounts.set(form.id, (formEntryCounts.get(form.id) || 0) + 1)
        validateFormFieldEntries(formEntry, form, formEntryError, validation)
      }
    }
    else {
      formEntryError.addEntryLevelError(FormEntryValidationErrorReason.FormRef)
    }
    if (formEntryHasErrors(formEntryError)) {
      validation.addFormEntryError(formEntryError)
    }
    return formEntryCounts
  }, new Map<FormId, number>(mageEvent.forms.map(x => [ x.id, 0 ])))
  let totalActiveFormEntryCount = 0
  for (const [ formId, formEntryCount ] of formEntryCounts) {
    const form = mageEvent.formFor(formId)!
    if (typeof form.min === 'number' && formEntryCount < form.min) {
      validation.addFormCountError(FormCountError.tooFewEntriesForForm(form))
    }
    else if (typeof form.max === 'number' && formEntryCount > form.max) {
      validation.addFormCountError(FormCountError.tooManyEntriesForForm(form))
    }
    totalActiveFormEntryCount += formEntryCount
  }
  if (typeof mageEvent.minObservationForms === 'number' && totalActiveFormEntryCount < mageEvent.minObservationForms) {
    validation.setTotalFormCountError(TotalFormCountError.tooFewFormEntries(mageEvent))
  }
  else if (typeof mageEvent.maxObservationForms === 'number' && totalActiveFormEntryCount > mageEvent.maxObservationForms) {
    validation.setTotalFormCountError(TotalFormCountError.tooManyFormEntries(mageEvent))
  }
  // TODO: invalidate new form entries that reference archived forms
  return validation
}

function formEntryHasErrors(err: FormEntryValidationError): boolean {
  return err.entryLevelErrors.size > 0 || err.fieldErrors.size > 0
}

function validateObservationAttachments(validation: ObservationValidationContext): ObservationValidationContext {
  const { observationAttrs, mageEvent } = validation
  const attachments = observationAttrs.attachments
  attachments.forEach((x, index) => {
    const invalid = validateAttachment(x, observationAttrs, mageEvent)
    if (invalid) {
      validation.setAttachmentError(index, invalid)
    }
  })
  return validation
}

function validateAttachment(attachment: Attachment, observation: ObservationAttrs, mageEvent: MageEvent): null | AttachmentValidationError {
  const formEntry = formEntryForId(attachment.observationFormId, observation)
  if (!formEntry) {
    return new AttachmentValidationError(
      AttachmentValidationErrorReason.FormEntryRef,
      `The attachment form entry reference ${attachment.observationFormId} is invalid.`)
  }
  const field = mageEvent.formFieldFor(attachment.fieldName, formEntry.formId)
  if (!field || field.type !== FormFieldType.Attachment) {
    return new AttachmentValidationError(
      AttachmentValidationErrorReason.FieldRef,
      `The attachment field reference ${attachment.fieldName} is invalid.`)
  }
  const dup = observation.attachments.find(x => x.id === attachment.id && x !== attachment)
  if (dup) {
    return new AttachmentValidationError(
      AttachmentValidationErrorReason.DuplicateId,
      `The attachment ID ${attachment.id} is not unique on the observation.`
    )
  }
  return null
}

interface FormFieldValidationContext {
  field: FormField,
  fieldEntry: FormFieldEntry,
  formEntry: FormEntry,
  observationAttrs: ObservationAttrs,
  mageEvent: MageEvent,
}

function FormFieldValidationResult(context: FormFieldValidationContext): fields.SimpleFieldValidationResult<FormFieldValidationError, FormFieldEntry | undefined> {
  return {
    failedBecauseTheEntry(reason: string, constraint: FieldConstraintKey = FieldConstraintKey.Value): FormFieldValidationError {
      return new FormFieldValidationError({ fieldName: context.field.name, message: `${context.field.name} ${reason}`, constraint: constraint })
    },
    succeeded(parsed?: FormFieldEntry): typeof parsed { return parsed }
  }
}

interface FormFieldValidationRule {
  (context: FormFieldValidationContext): FormFieldValidationError | FormFieldEntry | undefined
}

function validateRequiredThen(rule: FormFieldValidationRule): FormFieldValidationRule {
  return context => {
    const requiredError = fields.required.RequiredFieldValidation(context.field, context.fieldEntry, FormFieldValidationResult(context))
    if (requiredError) {
      return requiredError
    }
    return rule(context)
  }
}

// TODO: these could be improved to be more cleanly comosable in a functional way, e.g., monadic. i'll figure that out eventually
const FieldTypeValidationRules: { [type in FormFieldType]: FormFieldValidationRule } = {
  // attachments use min/max constraints, not required constraint
  [FormFieldType.Attachment]: context => fields.attachment.AttachmentFieldValidation(context.field, context.formEntry.id, context.observationAttrs, FormFieldValidationResult(context)),
  [FormFieldType.CheckBox]: validateRequiredThen(context => fields.checkbox.CheckboxFieldValidation(context.field, context.fieldEntry, FormFieldValidationResult(context))),
  [FormFieldType.DateTime]: validateRequiredThen(context => fields.date.DateFieldValidation(context.field, context.fieldEntry, FormFieldValidationResult(context))),
  [FormFieldType.Dropdown]: validateRequiredThen(context => fields.select.SelectFormFieldValidation(context.field, context.fieldEntry, FormFieldValidationResult(context))),
  [FormFieldType.Email]: validateRequiredThen(context => fields.email.EmailFieldValidation(context.field, context.fieldEntry, FormFieldValidationResult(context))),
  [FormFieldType.Geometry]: validateRequiredThen(context => fields.geometry.GeometryFieldValidation(context.field, context.fieldEntry, FormFieldValidationResult(context))),
  // TODO: no validation at all? legacy validation code did nothing for hidden fields
  [FormFieldType.Hidden]: context => null,
  [FormFieldType.MultiSelectDropdown]: validateRequiredThen(context => fields.multiselect.MultiSelectFormFieldValidation(context.field, context.fieldEntry, FormFieldValidationResult(context))),
  [FormFieldType.Numeric]: validateRequiredThen(context => fields.numeric.NumericFieldValidation(context.field, context.fieldEntry, FormFieldValidationResult(context))),
  [FormFieldType.Password]: validateRequiredThen(context => fields.text.TextFieldValidation(context.field, context.fieldEntry, FormFieldValidationResult(context))),
  [FormFieldType.Radio]: validateRequiredThen(context => fields.select.SelectFormFieldValidation(context.field, context.fieldEntry, FormFieldValidationResult(context))),
  [FormFieldType.Text]: validateRequiredThen(context => fields.text.TextFieldValidation(context.field, context.fieldEntry, FormFieldValidationResult(context))),
  [FormFieldType.TextArea]: validateRequiredThen(context => fields.text.TextFieldValidation(context.field, context.fieldEntry, FormFieldValidationResult(context))),
}

/**
 * TODO: This retains legacy functionality of only keying of form fields in the
 * event form to validate the values in the form entry.  However, this leaves
 * keys in the form entry that do not have a corresponding form field.  A
 * client could thus submit an observation with thousands of keys in a fomr
 * entry.
 */
function validateFormFieldEntries(formEntry: FormEntry, form: Form, formEntryError: FormEntryValidationError, validation: ObservationValidationContext): void {
  const { mageEvent, observationAttrs } = validation
  const formFields = form.fields || []
  const activeFields = formFields.filter(x => !x.archived)
  activeFields.forEach(field => {
    const fieldEntry = formEntry[field.name]
    const fieldValidation: FormFieldValidationContext = { field, fieldEntry, formEntry, mageEvent, observationAttrs }
    const resultEntry = FieldTypeValidationRules[field.type](fieldValidation)
    if (resultEntry instanceof FormFieldValidationError) {
      formEntryError.addFieldError(resultEntry)
    }
    else if (resultEntry !== void(0)) {
      formEntry[field.name] = resultEntry
    }
  })
}

class ObservationValidationContext {

  readonly observationAttrs: ObservationAttrs
  readonly mageEvent: MageEvent

  readonly #coreAttrsErrors: Map<ObservationValidationCoreAttrKey, string>
  readonly #formCountErrors: Map<FormId, FormCountError>
  readonly #formEntryErrors: Map<number, FormEntryValidationError>
  readonly #attachmentErrors: Map<number, AttachmentValidationError>
  #totalFormCountError: TotalFormCountError | null = null

  constructor(from: ObservationValidationContext)
  constructor(observationAttrs: ObservationAttrs, mageEvent: MageEvent)
  constructor(...args: [ from: ObservationValidationContext ] | [ attrs: ObservationAttrs, mageEvent: MageEvent ]) {
    const [ fromOrAttrs, maybeMageEvent ] = args
    if (fromOrAttrs instanceof ObservationValidationContext) {
      this.observationAttrs = fromOrAttrs.observationAttrs
      this.mageEvent = fromOrAttrs.mageEvent
      this.#coreAttrsErrors = new Map(fromOrAttrs.coreAttrsErrors)
      this.#formEntryErrors = new Map(fromOrAttrs.formEntryErrors)
      this.#formCountErrors = new Map(fromOrAttrs.formCountErrors)
      this.#attachmentErrors = new Map(fromOrAttrs.attachmentErrors)
    }
    else {
      this.observationAttrs = fromOrAttrs
      this.mageEvent = maybeMageEvent!
      this.#coreAttrsErrors = new Map()
      this.#formEntryErrors = new Map()
      this.#formCountErrors = new Map()
      this.#attachmentErrors = new Map()
    }
  }

  addCoreAttrsError(key: ObservationValidationCoreAttrKey, message: string): this {
    this.#coreAttrsErrors.set(key, message)
    return this
  }

  setTotalFormCountError(x: TotalFormCountError | null): this {
    this.#totalFormCountError = x
    return this
  }

  addFormCountError(x: FormCountError): this {
    this.#formCountErrors.set(x.formId, x)
    return this
  }

  addFormEntryError(formEntryError: FormEntryValidationError): this {
    this.#formEntryErrors.set(formEntryError.formEntryPosition, formEntryError)
    return this
  }

  setAttachmentError(index: number, err: AttachmentValidationError): this {
    this.#attachmentErrors.set(index, err)
    return this
  }

  get coreAttrsErrors(): Map<ObservationValidationCoreAttrKey, string> {
    return new Map(this.#coreAttrsErrors)
  }

  get formCountErrors(): Map<FormId, FormCountError> {
    return new Map(this.#formCountErrors)
  }

  get formEntryErrors(): Map<number, FormEntryValidationError> {
    return new Map(this.#formEntryErrors)
  }

  get attachmentErrors(): Map<number, AttachmentValidationError> {
    return new Map(this.#attachmentErrors)
  }

  get totalFormCountError(): TotalFormCountError | null {
    return this.#totalFormCountError
  }

  get hasErrors(): boolean {
    return this.#coreAttrsErrors.size > 0
      || this.#totalFormCountError !== null
      || this.#formCountErrors.size > 0
      || this.#formEntryErrors.size > 0
      || this.#attachmentErrors.size > 0
  }

  result(): ObservationValidationResult {
    const hasErrors = this.hasErrors
    const coreAttrsErrors = Object.freeze(Object.fromEntries(this.coreAttrsErrors))
    const totalFormCountError = this.totalFormCountError
    const formCountErrors = Object.freeze(Array.from(this.formCountErrors.entries()))
    const formEntryErrors = Object.freeze(Array.from(this.formEntryErrors.entries()))
    const attachmentErrors = Object.freeze(Array.from(this.attachmentErrors.entries()))
    return Object.freeze<ObservationValidationResult>({
      hasErrors,
      coreAttrsErrors,
      totalFormCountError,
      formCountErrors,
      formEntryErrors,
      attachmentErrors,
    })
  }
}

function AttachmentsRemovedDomainEvent(observation: Observation, removedAttachments: Attachment[]): AttachmentsRemovedDomainEvent {
  return Object.freeze<AttachmentsRemovedDomainEvent>({
    type: ObservationDomainEventType.AttachmentsRemoved,
    removedAttachments: Object.freeze(removedAttachments)
  })
}

function mergePendingDomainEvents(from: Observation, nextEvents: PendingObservationDomainEvent[]): PendingObservationDomainEvent[] {
  const removedAttachments = [] as Readonly<Attachment>[]
  const merged = [ ...from.pendingEvents, ...nextEvents ].reduce((merged, e) => {
    if (e.type === ObservationDomainEventType.AttachmentsRemoved) {
      removedAttachments.push(...e.removedAttachments)
      return merged
    }
    else {
      return [ ...merged, e ]
    }
  }, [] as PendingObservationDomainEvent[])
  if (removedAttachments.length) {
    merged.push(AttachmentsRemovedDomainEvent(from, removedAttachments))
  }
  return merged
}

redpajama_set_name: RedPajamaGithub


text:
Sony SupportKD-65X8505C
Google has announced that they will replace the Google Play Music service with YouTube Music from September 2020.
Starting in September 2020, Google Play Music will no longer be accessible on Android TVs. We want to inform you that you can use the YouTube Music service by transferring your account.
For more information, please visit the Google Play Music website.
To get you started, please visit:
How to transfer your Google Play Music account to YouTube Music
How to use YouTube Music with Android TV
We apologise for any inconvenience caused and ask you for your understanding.
What file formats are supported by Media Player from the Google Play Store?
The TV displays YouTube content that was viewed on another device
How do I access games found on Google Play?


The Jones Falls Expressway at night, seen from the Howard Street Bridge, in Baltimore, Maryland.
(BALTIMORE, Md. – Dec. 19, 2017) How many Marylanders will be on the roads this holiday season? The AAA Mid-Atlantic projects 2.3 million.
A boost in the labor market and economy is behind the growth in projected travelers compared to last holiday season.


text:
J.D. Horton
jdhorton@quinnemanuel.com
Direct Tel: +1 213-443-3167
Mr. Horton is a trial lawyer in the health care, product liability, intellectual property, media, and general business litigation areas. His practice includes the following substantive areas: health care, product liability, patent, First Amendment, privacy/publicity, and real estate.
In the health care field, Mr. Horton has represented such health plans as Kaiser Permanente, Blue Shield of California, Molina Healthcare and Universal Care.
In the product liability field, Mr. Horton has represented Colgate-Palmolive, Medicis, The Scotts Company, and others.
In the patent field, he has also represented the Johnson Controls, Inc., Seiko Epson Corporation, Freedom Wireless, and United Epitaxy Company.
In media-related matters, he has represented The Walt Disney Company and ABC.
In the real estate field, he has represented Playa Vista.
Represented Kaiser Foundation Health Plan in an enforcement proceeding in which the Department of Managed Health Care sought to require California health plans to cover Viagra and other sexual dysfunction drugs. The Court held the Department lacked legislative authority for such requirements. The decision was upheld on appeal.
Represented Blue Shield of California in a declaratory relief action against the Department of Managed Health Care where the Department sought to require California health plans to cover Xenical and other weight loss drugs. The Court granted summary judgment on behalf of Blue Shield of California.
Represented Blue Shield of California in a consumer class action alleging that Blue Shield of California misled its members by using "MediCare" reimbursement rates. The Court granted summary judgment on behalf of Blue Shield of California.
Represented Blue Shield of California in a consumer class action alleging that Blue Shield of California improperly coordinated emergency room benefits with its members' automobile "med-pay" policies. The Court granted Blue Shield of California's motion to dismiss.
Represented the Kaiser Foundation Health Plan in a 40-day trial arising out of an enforcement proceeding brought by the Department of Managed Health Care; the court rejected virtually the entirety of the Department's case.
Represented major HMO's in investigations and litigation being conducted by the California Department of Managed Health Care.
Represented a major health care service plan in an arbitration against a large IPO. After the health plan terminated the IPO, the IPO improperly attempt to persuade the health plan's Medicare members to sign with other health plans that were still contracted with the IPO. The matter was resolved favorably.
Currently representing a health insurance company in an investigation by the Department of Insurance in a claims audit.
Representing Colgate-Palmolive Company in cases in which plaintiffs claimed they developed mesothelioma by using body talcum powder products formerly manufactured and sold by Colgate. Quinn Emanuel took over the defense of two bellwether cases that were set for trial. We have obtained dismissals or summary judgments in many cases venued throughout the country.
Represented The Scotts Company in approximately 5,000 asbestos cases related to alleged tremolite asbestos and other non-regulated asbestiform amphiboles in vermiculite containing products. Cases were venued throughout the country.
Represented medical device company in personal injury action alleging need for heart transplant following injections of cosmetic dermal fillers. Prevailed on a motion to dismiss based upon federal preemption.
Represented Seiko Epson in one of the largest patent infringement cases ever filed with the International Trade Commission, asserting eleven patents and thirty-one claims against 1,000 different cartridge models sold by twenty-four manufacturers, importers and distributors of aftermarket ink cartridges for resale in the U.S. After a 7-day trial, the Administrative Law Judge found for the client on every asserted patent and claim, against every single accused product that was adjudicated, and against every respondent that had not already entered into a consent order, and based thereon, it issued a general exclusion order prohibiting all companies, whether or not they were parties to the ITC proceeding, from importing and selling infringing cartridges in the U.S.
Represented Freedom Wireless in a suit for patent infringement against twelve defendants for infringement of its patents on prepaid wireless telephone systems and methods. The trial lasted 15 weeks and a $128 million jury verdict was entered against several wireless telephone carriers. The verdict is the largest ever awarded in Massachusetts, and was the eighth biggest verdict awarded in the U.S. that year. The case recently settled for a lump sum payment of $87 million, plus on-going royalties which are expected to generate an additional $50 million.
Privacy and First Amendment Litigation
Represented The Walt Disney Company and ABC in a misappropriation of likeness action involving the alleged use of plaintiff's photographs without permission in a children's television show. Court granted summary judgment on behalf of The Walt Disney Company and ABC, and the decision was upheld on appeal.
Life Science: Mass Torts and Product Liability Litigation
(J.D., 1997)
(B.A., summa cum laude, Phi Beta Kappa, 1993)


Barbados Ambassador to United Nations, Liz Thompson.
UNITED NATIONS, Feb 12, CMC – Caribbean Community (CARICOM) countries Monday reiterated the need for a peaceful solution to the ongoing crisis in Venezuela even as they said they continue to follow closely "the unsatisfactory and increasingly volatile situation" in the South American country, reiterating their position of a policy of non-interference in the internal affairs of Caracas..
"It is with grave concern that we also follow the highly polarised and polarising circumstances surrounding Venezuela," Barbados Ambassador to the United Nations, Liz Thompson, said as she addressed a meeting of the Non-Aligned Movement (NAM) In the Trusteeship Council on Tuesday.
Speaking on behalf of the 15-member CARICOM grouping, the Barbados diplomat said the regional countries are also fully aware of "words of bitterness and blame coming from both sides.
"The thoughts and sabre rattling by the internal and external contenders in this deteriorating situation. In all of this we are most concerned for and motivated by the plight of the people of Venezuela, who have been rendered powerless pawns…while others play a form of geo-political chess and brinksmanship,"she told the meeting.
Last week, St. Kitts-Nevis Prime Minister Dr. Timothy Harris, led a CARICOM delegation to Uruguay for talks aimed at ending the political crisis in the South American country where Opposition Leader Juan Guaidó, backed by the United States and several other western countries, has declared himself the interim leader of Venezuela.
But Russia, China and Cuba are among countries that are supporting President Nicolas Maduro, who was sworn into office last month for a second consecutive term as head of state.
The governments of Mexico and Uruguay had called for the conference with representatives from the main countries and international organisations that hold a neutral position towards Venezuela.
Harris said the Montevideo Mechanism adopted at the conference presents "the only objective mechanism" to address the complex political situation in Venezuela.
The Montevideo Mechanism is regarded as the initiative in response to the call by the United Nations Secretary General, Antonio Guterres, to find a pathway to a peaceful resolution through dialogue and from a position of respect for International Law and Human Rights.
"We urge all parties inside and outside of Venezuela in the interest of the wellbeing of the people of that country to give the Montevideo Mechanism the time and space it needs to work. These United Nations know the painful experience of history both ancient and recent, the high and awful price of military intervention and the scourge of war," Thompson said, adding that the CARICOM countries do not choose "one side or the other.
"We choose principle, the principle which led to the founding of this United Nations and the inclusion in its charter of Article 24, which calls for member states to refrain from the threat or the use of force.
"The principle which birthed Article 21 of the Charter of the Organisation of American States which recognises territorial inviolability. The principles of human rights, international law of the rule of law, the sanctity of national sovereignty, and the pivotal principle of the peaceful settlement of disputes, the pursuit of peace, of dialogue and compromise to ensure the preservation of the dignity and worth of the human person".
Thompson told the conference that these principles are at the root of multilateralism.
"Indeed they are the very foundation on which this house in which we sit was built. Today we re-affirm our adherence to these principles. Critical for us too is the maintenance of the Caribbean and its wider region as a zone of peace.
"We remain steadfast in our view that economic strangulation and military intervention are not only counter to these principles, but will only exacerbate the already great suffering of the people of Venezuela".
The Barbadian diplomat said that it is the contention of the Caribbean that 'there must be a political solution that is crafted and owned by the Venezuelan people themselves and by their leaders.
"We support and call for a pathway to peace, forged not by threats but by dialogue. Not by escalating the tensions but by cooling them down. Not by marksmanship but by mediation. Not by the suffocation of sanctions but by the tools of diplomacy".
She said those who wished for a peaceful solution and the prosperity of Venezuelans must actively encourage dialogue "where both sides seat, both sides talk and both sides listen, so that the common ground on which they can move forward may be found".
Thompson said that it is in pursuit of this broad objective that CARICOM has offered itself to facilitate dialogue among all parties with a view to creating a peaceful resolution to the crisis in the South American country.
"We know too from the lesson of history …that peace and prosperity are indivisible. The people of Venezuela have already suffered enough. They deserve to live in peace. They deserve a future that offers prosperity. We urge all sides, difficult and complex though it may be, to enter into a constructive dialogue and to work together to build the pathway to peace for the Bolivarian republic of Venezuela," Thompson added.


If you were a bystander in this convenience store robbery and the robber started attacking the clerk, would you step in? Thinking through your responses and how your Active Self Protection comes into play is an important part of protecting yourself from a robbery!
What does this video shooting teach us about protecting ourselves against a robbery?
Since criminals are looking for victims and not fights, they tend to look for vulnerable people to target. This robbery certainly speaks to that! Think about a pride of lions chasing wildebeest in Africa and you get the picture. They target the elderly, the young, and the sick for easier attack and greater success. Likewise, robbers pick victims who they think will not effectively resist them, or that the criminal can quickly overwhelm. This includes the elderly, the young (kids and early teens), the sick, pregnant women, and the distracted. Men (the overwhelming majority of attackers) also target women because women tend to be physically weaker than men. If you're in a vulnerable population take extra precaution and train to defend against a robbery!
The Bystander Effect is real. There were several people around at the start of this robbery, but no one stepped in. You cannot count on anyone breaking the Bystander Effect to help you, especially if the attacker is armed and any help would be at a force deficit from it. Usually the best way to break the Bystander Effect is to have a force multiplication advantage, which is a great reason to keep your force multiplier on you at all times.
In any territorial or predatorial violence, the attacker gets to set the time and circumstances of the attack. They will almost always launch that attack from ambush, or as we like to call it in Umas, from "obscurity." Surviving that ambush is one of the most important keys to successfully defending yourself in a robbery. The clerk's coworkers responded once the robbery was launched, and helped her survive the ambush.
If you have spiritual fitness by knowing what you're willing to fight for and that you're willing and ready to win the fight no matter what, you place yourself way ahead of most attackers. Attackers are looking for victims and not looking for fights, so many times when an intended victim puts up a significant fight they will disengage and find other prey. This robbery is certainly evidence of that.
The root word of gunfight is "fight," not "gun." Whether you carry a firearm or not, recognize that you need to know how to fight and protect yourself against a robbery! Even if you do carry a firearm, you need empty-handed skills to be able to fight your way to your gun or defend yourself before you get the opportunity to draw. To think otherwise is madness. This robbery probably didn't rise to the level of deadly force, but physical force was definitely key. Learn how to fight with your hands!
If you have a partner with you when you're attacked (be it a LEO partner if you work on a team, or your spouse or martial artist buddy, or as in this robbery, coworkers), you want to do everything you can to work as a team. Knowing each other well and communicating clearly will help you protect yourself from danger and fight off an attacker, like the workers did in this robbery. This takes training and practice and commitment, but two or more partners working together present a formidable challenge to any attacker.
One of the challenges that is more and more common with multiple attackers is the concept of a "trailing accomplice," which we see in this robbery. What we see fairly often is a point man/gun man who launches the attack, and a trailing accomplice who observes from a distance and comes in when it's time to secure what they're after or after they identify any resistance. Just because you don't see multiple attackers at the beginning of an encounter doesn't mean they're not there, so if you're in a defensive encounter expect there to be multiple attackers.
As one of my martial arts mentors, Skip Hancock, is fond of saying, the ground must be your friend and not your enemy. You must know how to fight on the ground and not panic if the fight goes to the ground! You must have skills from bottom position, from top position, and in the scramble. You must be able to regain your feet and fight from wherever you find yourself. Too many fights require this skill to ignore it! The third coworker took the robber to the ground here, and you might need to do the same if ever you're in a robbery. Knowing how to use the ground to your advantage is important.